1 <?php
   2 
   3 /**
   4  * Klasse zur Verbindung und Interatkion mit der Datenbank.
   5  *
   6  * see http://net.tutsplus.com/tutorials/php/why-you-should-be-using-phps-pdo-for-database-access/
   7  *
   8  * @package redaxo\core\sql
   9  */
  10 class rex_sql implements Iterator
  11 {
  12     use rex_factory_trait;
  13 
  14     /**
  15      * Default SQL datetime format.
  16      */
  17     const FORMAT_DATETIME = 'Y-m-d H:i:s';
  18 
  19     /**
  20      * Controls query buffering.
  21      *
  22      * View `PDO::MYSQL_ATTR_USE_BUFFERED_QUERY` for more details.
  23      */
  24     const OPT_BUFFERED = 'buffered';
  25 
  26     protected $debug; // debug schalter
  27     protected $values; // Werte von setValue
  28     protected $rawValues; // Werte von setRawValue
  29     protected $fieldnames; // Spalten im ResultSet
  30     protected $rawFieldnames;
  31     protected $tablenames; // Tabelle im ResultSet
  32     protected $lastRow; // Wert der zuletzt gefetchten zeile
  33     protected $table; // Tabelle setzen
  34     protected $wherevar; // WHERE Bediengung
  35     protected $whereParams; // WHERE parameter array
  36     protected $rows; // anzahl der treffer
  37     protected $counter; // pointer
  38     protected $query; // Die Abfrage
  39     protected $params; // Die Abfrage-Parameter
  40     protected $DBID; // ID der Verbindung
  41 
  42     /** @var self[] */
  43     protected $records;
  44 
  45     /** @var PDOStatement */
  46     protected $stmt;
  47 
  48     /** @var PDO[] */
  49     protected static $pdo = [];
  50 
  51     /**
  52      * @param int $DBID
  53      *
  54      * @throws rex_sql_exception
  55      */
  56     protected function __construct($DBID = 1)
  57     {
  58         $this->debug = false;
  59         $this->flush();
  60         $this->selectDB($DBID);
  61     }
  62 
  63     /**
  64      * Stellt die Verbindung zur Datenbank her.
  65      *
  66      * @param int $DBID
  67      *
  68      * @throws rex_sql_exception
  69      */
  70     protected function selectDB($DBID)
  71     {
  72         $this->DBID = $DBID;
  73 
  74         try {
  75             if (!isset(self::$pdo[$DBID])) {
  76                 $dbconfig = rex::getProperty('db');
  77                 $conn = self::createConnection(
  78                     $dbconfig[$DBID]['host'],
  79                     $dbconfig[$DBID]['name'],
  80                     $dbconfig[$DBID]['login'],
  81                     $dbconfig[$DBID]['password'],
  82                     $dbconfig[$DBID]['persistent']
  83                 );
  84                 self::$pdo[$DBID] = $conn;
  85 
  86                 // ggf. Strict Mode abschalten
  87                 $this->setQuery('SET SESSION SQL_MODE="", NAMES utf8mb4');
  88             }
  89         } catch (PDOException $e) {
  90             throw new rex_sql_exception('Could not connect to database', $e, $this);
  91         }
  92     }
  93 
  94     /**
  95      * @param string $host
  96      * @param string $database
  97      * @param string $login
  98      * @param string $password
  99      * @param bool   $persistent
 100      *
 101      * @return PDO
 102      */
 103     protected static function createConnection($host, $database, $login, $password, $persistent = false)
 104     {
 105         if (!$database) {
 106             throw new InvalidArgumentException('Database name can not be empty.');
 107         }
 108 
 109         $dsn = 'mysql:host=' . $host . ';dbname=' . $database;
 110         $options = [
 111             PDO::ATTR_PERSISTENT => (bool) $persistent,
 112             PDO::ATTR_FETCH_TABLE_NAMES => true,
 113             // PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL,
 114             // PDO::ATTR_EMULATE_PREPARES => true,
 115         ];
 116 
 117         $dbh = @new PDO($dsn, $login, $password, $options);
 118         $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 119         return $dbh;
 120     }
 121 
 122     /**
 123      * Gibt die DatenbankId der Abfrage (SQL) zurueck,
 124      * oder false wenn die Abfrage keine DBID enthaelt.
 125      *
 126      * @param string $qry
 127      *
 128      * @return bool
 129      */
 130     protected static function getQueryDBID($qry)
 131     {
 132         $qry = trim($qry);
 133 
 134         if (preg_match('/\(DB([1-9]){1}\)/i', $qry, $matches)) {
 135             return $matches[1];
 136         }
 137 
 138         return false;
 139     }
 140 
 141     /**
 142      * Entfernt die DBID aus einer Abfrage (SQL) und gibt die DBID zurueck falls
 143      * vorhanden, sonst false.
 144      *
 145      * @param string $qry Abfrage
 146      *
 147      * @return string
 148      */
 149     protected static function stripQueryDBID(&$qry)
 150     {
 151         $qry = trim($qry);
 152 
 153         if (($qryDBID = self::getQueryDBID($qry)) !== false) {
 154             $qry = substr($qry, 6);
 155         }
 156 
 157         return $qryDBID;
 158     }
 159 
 160     /**
 161      * Gibt den Typ der Abfrage (SQL) zurueck,
 162      * oder false wenn die Abfrage keinen Typ enthaelt.
 163      *
 164      * Moegliche Typen:
 165      * - SELECT
 166      * - SHOW
 167      * - UPDATE
 168      * - INSERT
 169      * - DELETE
 170      * - REPLACE
 171      * - CREATE
 172      * - CALL
 173      * - OPTIMIZE
 174      *
 175      * @param string $qry
 176      *
 177      * @return bool|string
 178      */
 179     public static function getQueryType($qry)
 180     {
 181         $qry = trim($qry);
 182         // DBID aus dem Query herausschneiden, falls vorhanden
 183         self::stripQueryDBID($qry);
 184 
 185         if (preg_match('/^(SELECT|SHOW|UPDATE|INSERT|DELETE|REPLACE|CREATE|CALL|OPTIMIZE)/i', $qry, $matches)) {
 186             return strtoupper($matches[1]);
 187         }
 188 
 189         return false;
 190     }
 191 
 192     /**
 193      * Returns a datetime string in sql datetime format (Y-m-d H:i:s) using the given timestamp or the current time
 194      * if no timestamp (or `null`) is given.
 195      *
 196      * @param int|null $timestamp
 197      *
 198      * @return string
 199      */
 200     public static function datetime($timestamp = null)
 201     {
 202         return date(self::FORMAT_DATETIME, null === $timestamp ? time() : $timestamp);
 203     }
 204 
 205     /**
 206      * Setzt eine Abfrage (SQL) ab, wechselt die DBID falls vorhanden.
 207      *
 208      * @param string $query   The sql-query
 209      * @param array  $params  An optional array of statement parameter
 210      * @param array  $options For possible option keys view `rex_sql::OPT_*` constants
 211      *
 212      * @return $this
 213      *
 214      * @throws rex_sql_exception on errors
 215      */
 216     public function setDBQuery($query, array $params = [], array $options = [])
 217     {
 218         // save origin connection-id
 219         $oldDBID = $this->DBID;
 220 
 221         // change connection-id but only for this one query
 222         if (($qryDBID = self::stripQueryDBID($query)) !== false) {
 223             $this->selectDB($qryDBID);
 224         }
 225 
 226         $this->setQuery($query, $params, $options);
 227 
 228         // restore connection-id
 229         $this->DBID = $oldDBID;
 230 
 231         return $this;
 232     }
 233 
 234     /**
 235      * Setzt Debugmodus an/aus.
 236      *
 237      * @param bool $debug Debug TRUE/FALSE
 238      *
 239      * @return $this the current rex_sql object
 240      */
 241     public function setDebug($debug = true)
 242     {
 243         $this->debug = $debug;
 244 
 245         return $this;
 246     }
 247 
 248     /**
 249      * Prepares a PDOStatement.
 250      *
 251      * @param string $qry A query string with placeholders
 252      *
 253      * @throws rex_sql_exception
 254      *
 255      * @return PDOStatement The prepared statement
 256      */
 257     public function prepareQuery($qry)
 258     {
 259         $pdo = self::$pdo[$this->DBID];
 260         try {
 261             $this->query = $qry;
 262             $this->stmt = $pdo->prepare($qry);
 263             return $this->stmt;
 264         } catch (PDOException $e) {
 265             throw new rex_sql_exception('Error while preparing statement "' . $qry . '"! ' . $e->getMessage(), $e, $this);
 266         }
 267     }
 268 
 269     /**
 270      * Executes the prepared statement with the given input parameters.
 271      *
 272      * @param array $params  Array of input parameters
 273      * @param array $options For possible option keys view `rex_sql::OPT_*` constants
 274      *
 275      * @return $this
 276      *
 277      * @throws rex_sql_exception
 278      */
 279     public function execute(array $params = [], array $options = [])
 280     {
 281         if (!$this->stmt) {
 282             throw new rex_sql_exception('you need to prepare a query before calling execute()', null, $this);
 283         }
 284 
 285         $buffered = null;
 286         $pdo = self::$pdo[$this->DBID];
 287         if (isset($options[self::OPT_BUFFERED])) {
 288             $buffered = $pdo->getAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY);
 289             $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $options[self::OPT_BUFFERED]);
 290         }
 291 
 292         try {
 293             $this->flush();
 294             $this->params = $params;
 295 
 296             $this->stmt->execute($params);
 297             $this->rows = $this->stmt->rowCount();
 298         } catch (PDOException $e) {
 299             throw new rex_sql_exception('Error while executing statement "' . $this->query . '" using params ' . json_encode($params) . '! ' . $e->getMessage(), $e, $this);
 300         } finally {
 301             if (null !== $buffered) {
 302                 $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $buffered);
 303             }
 304 
 305             if ($this->debug) {
 306                 $this->printError($this->query, $params);
 307             }
 308         }
 309 
 310         return $this;
 311     }
 312 
 313     /**
 314      * Executes the given sql-query.
 315      *
 316      * If parameters will be provided, a prepared statement will be executed.
 317      *
 318      * example 1:
 319      *    $sql->setQuery('SELECT * FROM mytable where id=:id', ['id' => 3]);
 320      *
 321      * NOTE: named-parameters/?-placeholders are not supported in LIMIT clause!
 322      *
 323      * @param string $query   The sql-query
 324      * @param array  $params  An optional array of statement parameter
 325      * @param array  $options For possible option keys view `rex_sql::OPT_*` constants
 326      *
 327      * @return $this
 328      *
 329      * @throws rex_sql_exception on errors
 330      */
 331     public function setQuery($query, array $params = [], array $options = [])
 332     {
 333         // Alle Werte zuruecksetzen
 334         $this->flush();
 335         $this->query = $query;
 336         $this->params = $params;
 337         $this->stmt = null;
 338 
 339         if (!empty($params)) {
 340             $this->prepareQuery($query);
 341             $this->execute($params, $options);
 342 
 343             return $this;
 344         }
 345 
 346         $buffered = null;
 347         $pdo = self::$pdo[$this->DBID];
 348         if (isset($options[self::OPT_BUFFERED])) {
 349             $buffered = $pdo->getAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY);
 350             $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $options[self::OPT_BUFFERED]);
 351         }
 352 
 353         try {
 354             $this->stmt = rex_timer::measure(__METHOD__, function () use ($pdo, $query) {
 355                 return $pdo->query($query);
 356             });
 357 
 358             $this->rows = $this->stmt->rowCount();
 359         } catch (PDOException $e) {
 360             throw new rex_sql_exception('Error while executing statement "' . $query . '"! ' . $e->getMessage(), $e, $this);
 361         } finally {
 362             if (null !== $buffered) {
 363                 $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $buffered);
 364             }
 365 
 366             if ($this->debug) {
 367                 $this->printError($query, $params);
 368             }
 369         }
 370 
 371         return $this;
 372     }
 373 
 374     /**
 375      * Setzt den Tabellennamen.
 376      *
 377      * @param string $table Tabellenname
 378      *
 379      * @return $this the current rex_sql object
 380      */
 381     public function setTable($table)
 382     {
 383         $this->table = $table;
 384 
 385         return $this;
 386     }
 387 
 388     /**
 389      * Sets the raw value of a column.
 390      *
 391      * @param string $colName Name of the column
 392      * @param string $value   The raw value
 393      *
 394      * @return $this the current rex_sql object
 395      */
 396     public function setRawValue($colName, $value)
 397     {
 398         $this->rawValues[$colName] = $value;
 399         unset($this->values[$colName]);
 400 
 401         return $this;
 402     }
 403 
 404     /**
 405      * Set the value of a column.
 406      *
 407      * @param string $colName Name of the column
 408      * @param mixed  $value   The value
 409      *
 410      * @return $this the current rex_sql object
 411      */
 412     public function setValue($colName, $value)
 413     {
 414         $this->values[$colName] = $value;
 415         unset($this->rawValues[$colName]);
 416 
 417         return $this;
 418     }
 419 
 420     /**
 421      * Set the array value of a column (json encoded).
 422      *
 423      * @param string $colName Name of the column
 424      * @param array  $value   The value
 425      *
 426      * @return $this the current rex_sql object
 427      */
 428     public function setArrayValue($colName, array $value)
 429     {
 430         return $this->setValue($colName, json_encode($value));
 431     }
 432 
 433     /**
 434      * Sets the datetime value of a column.
 435      *
 436      * @param string   $colName   Name of the column
 437      * @param int|null $timestamp Unix timestamp (if `null` is given, the current time is used)
 438      *
 439      * @return $this the current rex_sql object
 440      */
 441     public function setDateTimeValue($colName, $timestamp)
 442     {
 443         return $this->setValue($colName, self::datetime($timestamp));
 444     }
 445 
 446     /**
 447      * Setzt ein Array von Werten zugleich.
 448      *
 449      * @param array $valueArray Ein Array von Werten
 450      *
 451      * @return $this the current rex_sql object
 452      */
 453     public function setValues(array $valueArray)
 454     {
 455         foreach ($valueArray as $name => $value) {
 456             $this->setValue($name, $value);
 457         }
 458 
 459         return $this;
 460     }
 461 
 462     /**
 463      * Returns whether values are set inside this rex_sql object.
 464      *
 465      * @return bool True if value isset and not null, otherwise False
 466      */
 467     public function hasValues()
 468     {
 469         return !empty($this->values);
 470     }
 471 
 472     /**
 473      * Prueft den Wert einer Spalte der aktuellen Zeile ob ein Wert enthalten ist.
 474      *
 475      * @param string $feld Spaltenname des zu pruefenden Feldes
 476      * @param string $prop Wert, der enthalten sein soll
 477      *
 478      * @return bool
 479      *
 480      * @throws rex_sql_exception
 481      */
 482     protected function isValueOf($feld, $prop)
 483     {
 484         if ($prop == '') {
 485             return true;
 486         }
 487         return strpos($this->getValue($feld), $prop) !== false;
 488     }
 489 
 490     /**
 491      * Adds a record for multi row/batch operations.
 492      *
 493      * This method can only be used in combination with `insert()` and `replace()`.
 494      *
 495      * Example:
 496      *      $sql->addRecord(function (rex_sql $record) {
 497      *          $record->setValue('title', 'Foo');
 498      *          $record->setRawValue('created', 'NOW()');
 499      *      });
 500      *
 501      * @param callable $callback The callback receives a new `rex_sql` instance for the new record
 502      *                           and must set the values of the new record on that instance (see example above)
 503      *
 504      * @return $this
 505      */
 506     public function addRecord(callable $callback)
 507     {
 508         $record = self::factory($this->DBID);
 509 
 510         $callback($record);
 511 
 512         $this->records[] = $record;
 513 
 514         return $this;
 515     }
 516 
 517     /**
 518      * Setzt die WHERE Bedienung der Abfrage.
 519      *
 520      * example 1:
 521      *    $sql->setWhere(['id' => 3, 'field' => '']); // results in id = 3 AND field = ''
 522      *    $sql->setWhere([['id' => 3, 'field' => '']]); // results in id = 3 OR field = ''
 523      *
 524      * example 2:
 525      *    $sql->setWhere('myid = :id OR anotherfield = :field', ['id' => 3, 'field' => '']);
 526      *
 527      * example 3 (deprecated):
 528      *    $sql->setWhere('myid="35" OR abc="zdf"');
 529      *
 530      * @param string|array $where
 531      * @param array        $whereParams
 532      *
 533      * @throws rex_sql_exception
 534      *
 535      * @return $this the current rex_sql object
 536      */
 537     public function setWhere($where, $whereParams = null)
 538     {
 539         if (is_array($where)) {
 540             $this->wherevar = 'WHERE ' . $this->buildWhereArg($where);
 541             $this->whereParams = $where;
 542         } elseif (is_string($where) && is_array($whereParams)) {
 543             $this->wherevar = 'WHERE ' . $where;
 544             $this->whereParams = $whereParams;
 545         } elseif (is_string($where)) {
 546             //$trace = debug_backtrace();
 547             //$loc = $trace[0];
 548             //trigger_error('you have to take care to provide escaped values for your where-string in file "'. $loc['file'] .'" on line '. $loc['line'] .'!', E_USER_WARNING);
 549 
 550             $this->wherevar = 'WHERE ' . $where;
 551             $this->whereParams = [];
 552         } else {
 553             throw new rex_sql_exception('expecting $where to be an array, "' . gettype($where) . '" given!', null, $this);
 554         }
 555 
 556         return $this;
 557     }
 558 
 559     /**
 560      * Concats the given array to a sql condition using bound parameters.
 561      * AND/OR opartors are alternated depending on $level.
 562      *
 563      * @param array $arrFields
 564      * @param int   $level
 565      *
 566      * @return string
 567      */
 568     private function buildWhereArg(array $arrFields, $level = 0)
 569     {
 570         if ($level % 2 == 1) {
 571             $op = ' OR ';
 572         } else {
 573             $op = ' AND ';
 574         }
 575 
 576         $qry = '';
 577         foreach ($arrFields as $fld_name => $value) {
 578             if (is_array($value)) {
 579                 $arg = '(' . $this->buildWhereArg($value, $level + 1) . ')';
 580             } else {
 581                 $arg = $this->escapeIdentifier($fld_name) . ' = :' . $fld_name;
 582             }
 583 
 584             if ($qry != '') {
 585                 $qry .= $op;
 586             }
 587             $qry .= $arg;
 588         }
 589         return $qry;
 590     }
 591 
 592     /**
 593      * Returns the value of a column.
 594      *
 595      * @param string $colName Name of the column
 596      *
 597      * @throws rex_sql_exception
 598      *
 599      * @return mixed
 600      */
 601     public function getValue($colName)
 602     {
 603         if (empty($colName)) {
 604             throw new rex_sql_exception('parameter fieldname must not be empty!', null, $this);
 605         }
 606 
 607         // fast fail,... value already set manually?
 608         if (isset($this->values[$colName])) {
 609             return $this->values[$colName];
 610         }
 611 
 612         // check if there is an table alias defined
 613         // if not, try to guess the tablename
 614         if (strpos($colName, '.') === false) {
 615             $tables = $this->getTablenames();
 616             foreach ($tables as $table) {
 617                 if (in_array($table . '.' . $colName, $this->rawFieldnames)) {
 618                     return $this->fetchValue($table . '.' . $colName);
 619                 }
 620             }
 621         }
 622 
 623         return $this->fetchValue($colName);
 624     }
 625 
 626     /**
 627      * Returns the array value of a (json encoded) column.
 628      *
 629      * @param string $colName Name of the column
 630      *
 631      * @return array
 632      *
 633      * @throws rex_sql_exception
 634      */
 635     public function getArrayValue($colName)
 636     {
 637         return json_decode($this->getValue($colName), true);
 638     }
 639 
 640     /**
 641      * Returns the unix timestamp of a datetime column.
 642      *
 643      * @param string $colName Name of the column
 644      *
 645      * @return int|null Unix timestamp or `null` if the column is `null` or not in sql datetime format
 646      *
 647      * @throws rex_sql_exception
 648      */
 649     public function getDateTimeValue($colName)
 650     {
 651         $value = $this->getValue($colName);
 652         return $value ? strtotime($value) : null;
 653     }
 654 
 655     /**
 656      * @param string $feldname
 657      *
 658      * @return mixed
 659      */
 660     protected function fetchValue($feldname)
 661     {
 662         if (isset($this->values[$feldname])) {
 663             return $this->values[$feldname];
 664         }
 665 
 666         if (empty($this->lastRow)) {
 667             // no row fetched, but also no query was executed before
 668             if ($this->stmt == null) {
 669                 return null;
 670             }
 671             $this->lastRow = $this->stmt->fetch(PDO::FETCH_ASSOC);
 672         }
 673 
 674         // isset() alone doesn't work here, because values may also be null
 675         if (is_array($this->lastRow) && (isset($this->lastRow[$feldname]) || array_key_exists($feldname, $this->lastRow))) {
 676             return $this->lastRow[$feldname];
 677         }
 678         trigger_error('Field "' . $feldname . '" does not exist in result!', E_USER_WARNING);
 679         return null;
 680     }
 681 
 682     /**
 683      * Gibt den Wert der aktuellen Zeile im ResultSet zurueck
 684      * Falls es noch keine erste Zeile (lastRow) gibt, wird der Satzzeiger
 685      * initialisiert. Weitere Satzwechsel mittels next().
 686      *
 687      * @param int $fetch_type
 688      *
 689      * @return mixed
 690      */
 691     public function getRow($fetch_type = PDO::FETCH_ASSOC)
 692     {
 693         if (!$this->lastRow) {
 694             $this->lastRow = $this->stmt->fetch($fetch_type);
 695         }
 696         return $this->lastRow;
 697     }
 698 
 699     /**
 700      * Prueft, ob eine Spalte im Resultset vorhanden ist.
 701      *
 702      * @param string $feldname Name der Spalte
 703      *
 704      * @return bool
 705      */
 706     public function hasValue($feldname)
 707     {
 708         // fast fail,... value already set manually?
 709         if (isset($this->values[$feldname])) {
 710             return true;
 711         }
 712 
 713         if (strpos($feldname, '.') !== false) {
 714             $parts = explode('.', $feldname);
 715             return in_array($parts[0], $this->getTablenames()) && in_array($parts[1], $this->getFieldnames());
 716         }
 717         return in_array($feldname, $this->getFieldnames());
 718     }
 719 
 720     /**
 721      * Prueft, ob das Feld mit dem Namen $feldname Null ist.
 722      *
 723      * Falls das Feld nicht vorhanden ist,
 724      * wird Null zurueckgegeben, sonst True/False
 725      *
 726      * @param string $feldname
 727      *
 728      * @return bool|null
 729      *
 730      * @throws rex_sql_exception
 731      */
 732     public function isNull($feldname)
 733     {
 734         if ($this->hasValue($feldname)) {
 735             return $this->getValue($feldname) === null;
 736         }
 737 
 738         return null;
 739     }
 740 
 741     /**
 742      * Gibt die Anzahl der Zeilen zurueck.
 743      *
 744      * @return null|int
 745      */
 746     public function getRows()
 747     {
 748         return $this->rows;
 749     }
 750 
 751     /**
 752      * Gibt die Anzahl der Felder/Spalten zurueck.
 753      *
 754      * @return int
 755      */
 756     public function getFields()
 757     {
 758         return $this->stmt ? $this->stmt->columnCount() : 0;
 759     }
 760 
 761     /**
 762      * Baut den SET bestandteil mit der
 763      * verfuegbaren values zusammen und gibt diesen zurueck.
 764      *
 765      * @see setValue
 766      *
 767      * @return string
 768      */
 769     protected function buildPreparedValues()
 770     {
 771         $qry = '';
 772         if (is_array($this->values)) {
 773             foreach ($this->values as $fld_name => $value) {
 774                 if ($qry != '') {
 775                     $qry .= ', ';
 776                 }
 777 
 778                 $qry .= $this->escapeIdentifier($fld_name) .' = :' . $fld_name;
 779             }
 780         }
 781         if (is_array($this->rawValues)) {
 782             foreach ($this->rawValues as $fld_name => $value) {
 783                 if ($qry != '') {
 784                     $qry .= ', ';
 785                 }
 786 
 787                 $qry .= $this->escapeIdentifier($fld_name) . ' = ' . $value;
 788             }
 789         }
 790 
 791         if (trim($qry) == '') {
 792             // FIXME
 793             trigger_error('no values given to buildPreparedValues for update(), insert() or replace()', E_USER_WARNING);
 794         }
 795 
 796         return $qry;
 797     }
 798 
 799     /**
 800      * @return string
 801      */
 802     public function getWhere()
 803     {
 804         // we have an custom where criteria, so we don't need to build one automatically
 805         if ($this->wherevar != '') {
 806             return $this->wherevar;
 807         }
 808 
 809         return '';
 810     }
 811 
 812     /**
 813      * Setzt eine Select-Anweisung auf die angegebene Tabelle
 814      * mit den WHERE Parametern ab.
 815      *
 816      * @param string $fields
 817      *
 818      * @return $this
 819      *
 820      * @throws rex_sql_exception
 821      */
 822     public function select($fields = '*')
 823     {
 824         $this->setQuery(
 825             'SELECT ' . $fields . ' FROM ' . $this->escapeIdentifier($this->table) . ' ' . $this->getWhere(),
 826             $this->whereParams
 827         );
 828         return $this;
 829     }
 830 
 831     /**
 832      * Setzt eine Update-Anweisung auf die angegebene Tabelle
 833      * mit den angegebenen Werten und WHERE Parametern ab.
 834      *
 835      * @return $this
 836      *
 837      * @throws rex_sql_exception
 838      */
 839     public function update()
 840     {
 841         $this->setQuery(
 842             'UPDATE ' . $this->escapeIdentifier($this->table) . ' SET ' . $this->buildPreparedValues() . ' ' . $this->getWhere(),
 843             array_merge($this->values, $this->whereParams)
 844         );
 845         return $this;
 846     }
 847 
 848     /**
 849      * Setzt eine Insert-Anweisung auf die angegebene Tabelle
 850      * mit den angegebenen Werten ab.
 851      *
 852      * @return $this
 853      *
 854      * @throws rex_sql_exception
 855      */
 856     public function insert()
 857     {
 858         if ($this->records) {
 859             return $this->setMultiRecordQuery('INSERT');
 860         }
 861 
 862         // hold a copies of the query fields for later debug out (the class property will be reverted in setQuery())
 863         $tableName = $this->table;
 864         $values = $this->values;
 865 
 866         if ($this->values || $this->rawValues) {
 867             $setValues = 'SET '.$this->buildPreparedValues();
 868         } else {
 869             $setValues = 'VALUES ()';
 870         }
 871 
 872         $this->setQuery(
 873             'INSERT INTO ' . $this->escapeIdentifier($this->table) . ' ' . $setValues,
 874             $this->values
 875         );
 876 
 877         // provide debug infos, if insert is considered successfull, but no rows were inserted.
 878         // this happens when you violate against a NOTNULL constraint
 879         if ($this->getRows() == 0) {
 880             throw new rex_sql_exception('Error while inserting into table "' . $tableName . '" with values ' . print_r($values, true) . '! Check your null/not-null constraints!', null, $this);
 881         }
 882         return $this;
 883     }
 884 
 885     /**
 886      * @return $this|rex_sql
 887      *
 888      * @throws rex_sql_exception
 889      */
 890     public function insertOrUpdate()
 891     {
 892         if ($this->records) {
 893             return $this->setMultiRecordQuery('INSERT', true);
 894         }
 895 
 896         // hold a copies of the query fields for later debug out (the class property will be reverted in setQuery())
 897         $tableName = $this->table;
 898         $values = $this->values;
 899 
 900         $onDuplicateKeyUpdate = $this->buildOnDuplicateKeyUpdate(array_keys(array_merge($this->values, $this->rawValues)));
 901         $this->setQuery(
 902             'INSERT INTO ' . $this->escapeIdentifier($this->table) . ' SET ' . $this->buildPreparedValues() . ' ' . $onDuplicateKeyUpdate,
 903             $this->values
 904         );
 905 
 906         // provide debug infos, if insert is considered successfull, but no rows were inserted.
 907         // this happens when you violate against a NOTNULL constraint
 908         if ($this->getRows() == 0) {
 909             throw new rex_sql_exception('Error while inserting into table "' . $tableName . '" with values ' . print_r($values, true) . '! Check your null/not-null constraints!', null, $this);
 910         }
 911         return $this;
 912     }
 913 
 914     /**
 915      * Setzt eine Replace-Anweisung auf die angegebene Tabelle
 916      * mit den angegebenen Werten ab.
 917      *
 918      * @return $this
 919      *
 920      * @throws rex_sql_exception
 921      */
 922     public function replace()
 923     {
 924         if ($this->records) {
 925             return $this->setMultiRecordQuery('REPLACE');
 926         }
 927 
 928         $this->setQuery(
 929             'REPLACE INTO ' . $this->escapeIdentifier($this->table) . ' SET ' . $this->buildPreparedValues() . ' ' . $this->getWhere(),
 930             array_merge($this->values, $this->whereParams)
 931         );
 932         return $this;
 933     }
 934 
 935     /**
 936      * Setzt eine Delete-Anweisung auf die angegebene Tabelle
 937      * mit den angegebenen WHERE Parametern ab.
 938      *
 939      * @return $this
 940      *
 941      * @throws rex_sql_exception
 942      */
 943     public function delete()
 944     {
 945         $this->setQuery(
 946             'DELETE FROM ' . $this->escapeIdentifier($this->table) . ' ' . $this->getWhere(),
 947             $this->whereParams
 948         );
 949         return $this;
 950     }
 951 
 952     /**
 953      * Stellt alle Werte auf den Ursprungszustand zurueck.
 954      *
 955      * @return $this the current rex_sql object
 956      */
 957     private function flush()
 958     {
 959         $this->values = [];
 960         $this->rawValues = [];
 961         $this->records = [];
 962         $this->whereParams = [];
 963         $this->lastRow = [];
 964         $this->fieldnames = null;
 965         $this->rawFieldnames = null;
 966         $this->tablenames = null;
 967 
 968         $this->table = '';
 969         $this->wherevar = '';
 970         $this->counter = 0;
 971         $this->rows = 0;
 972 
 973         return $this;
 974     }
 975 
 976     /**
 977      * Stellt alle Values, die mit setValue() gesetzt wurden, zurueck.
 978      *
 979      * @see setValue(), #getValue()
 980      *
 981      * @return $this the current rex_sql object
 982      */
 983     public function flushValues()
 984     {
 985         $this->values = [];
 986         $this->rawValues = [];
 987 
 988         return $this;
 989     }
 990 
 991     /**
 992      * Prueft ob das Resultset weitere Datensaetze enthaelt.
 993      */
 994     public function hasNext()
 995     {
 996         return $this->counter < $this->rows;
 997     }
 998 
 999     /**
1000      * Setzt den Cursor des Resultsets zurueck zum Anfang.
1001      *
1002      * @return $this the current rex_sql object
1003      *
1004      * @throws rex_sql_exception
1005      */
1006     public function reset()
1007     {
1008         // re-execute the statement
1009         if ($this->stmt && $this->counter != 0) {
1010             $this->execute($this->params);
1011             $this->counter = 0;
1012         }
1013 
1014         return $this;
1015     }
1016 
1017     /**
1018      * Gibt die letzte InsertId zurueck.
1019      */
1020     public function getLastId()
1021     {
1022         return self::$pdo[$this->DBID]->lastInsertId();
1023     }
1024 
1025     /**
1026      * Laedt das komplette Resultset in ein Array und gibt dieses zurueck und
1027      * wechselt die DBID falls vorhanden.
1028      *
1029      * @param string $query     The sql-query
1030      * @param array  $params    An optional array of statement parameter
1031      * @param int    $fetchType
1032      *
1033      * @return array
1034      *
1035      * @throws rex_sql_exception on errors
1036      */
1037     public function getDBArray($query = null, array $params = [], $fetchType = PDO::FETCH_ASSOC)
1038     {
1039         if (!$query) {
1040             $query = $this->query;
1041             $params = $this->params;
1042         }
1043 
1044         $pdo = self::$pdo[$this->DBID];
1045 
1046         $pdo->setAttribute(PDO::ATTR_FETCH_TABLE_NAMES, false);
1047         $this->setDBQuery($query, $params);
1048         $pdo->setAttribute(PDO::ATTR_FETCH_TABLE_NAMES, true);
1049 
1050         return $this->stmt->fetchAll($fetchType);
1051     }
1052 
1053     /**
1054      * Laedt das komplette Resultset in ein Array und gibt dieses zurueck.
1055      *
1056      * @param string $query     The sql-query
1057      * @param array  $params    An optional array of statement parameter
1058      * @param int    $fetchType
1059      *
1060      * @return array
1061      *
1062      * @throws rex_sql_exception on errors
1063      */
1064     public function getArray($query = null, array $params = [], $fetchType = PDO::FETCH_ASSOC)
1065     {
1066         if (!$query) {
1067             $query = $this->query;
1068             $params = $this->params;
1069         }
1070 
1071         $pdo = self::$pdo[$this->DBID];
1072 
1073         $pdo->setAttribute(PDO::ATTR_FETCH_TABLE_NAMES, false);
1074         $this->setQuery($query, $params);
1075         $pdo->setAttribute(PDO::ATTR_FETCH_TABLE_NAMES, true);
1076 
1077         return $this->stmt->fetchAll($fetchType);
1078     }
1079 
1080     /**
1081      * Gibt die zuletzt aufgetretene Fehlernummer zurueck.
1082      *
1083      * @return string
1084      */
1085     public function getErrno()
1086     {
1087         return $this->stmt ? $this->stmt->errorCode() : self::$pdo[$this->DBID]->errorCode();
1088     }
1089 
1090     /**
1091      * @return int
1092      */
1093     public function getMysqlErrno()
1094     {
1095         $errorInfos = $this->stmt ? $this->stmt->errorInfo() : self::$pdo[$this->DBID]->errorInfo();
1096 
1097         return (int) $errorInfos[1];
1098     }
1099 
1100     /**
1101      * Gibt den zuletzt aufgetretene Fehler zurueck.
1102      */
1103     public function getError()
1104     {
1105         $errorInfos = $this->stmt ? $this->stmt->errorInfo() : self::$pdo[$this->DBID]->errorInfo();
1106         // idx0   SQLSTATE error code (a five characters alphanumeric identifier defined in the ANSI SQL standard).
1107         // idx1   Driver-specific error code.
1108         // idx2   Driver-specific error message.
1109         return $errorInfos[2];
1110     }
1111 
1112     /**
1113      * Prueft, ob ein Fehler aufgetreten ist.
1114      *
1115      * @return bool
1116      */
1117     public function hasError()
1118     {
1119         return $this->getErrno() != 0;
1120     }
1121 
1122     /**
1123      * Gibt die letzte Fehlermeldung aus.
1124      *
1125      * @param string $qry
1126      * @param array  $params
1127      */
1128     protected function printError($qry, $params)
1129     {
1130         $errors['query'] = $qry;
1131         if (!empty($params)) {
1132             $errors['params'] = $params;
1133 
1134             // taken from https://github.com/doctrine/DoctrineBundle/blob/d57c1a35cd32e6b942fdda90ae3888cc1bb41e6b/Twig/DoctrineExtension.php#L290-L305
1135             $i = 0;
1136             $errors['fullquery'] = preg_replace_callback(
1137                 '/\?|((?<!:):[a-z0-9_]+)/i',
1138                 static function ($matches) use ($params, &$i) {
1139                     $key = substr($matches[0], 1);
1140                     if (!array_key_exists($i, $params) && ($key === false || !array_key_exists($key, $params))) {
1141                         return $matches[0];
1142                     }
1143                     $value = array_key_exists($i, $params) ? $params[$i] : $params[$key];
1144                     $result = self::factory()->escape($value);
1145                     ++$i;
1146                     return $result;
1147                 },
1148                 $qry
1149             );
1150         }
1151         if ($this->getRows()) {
1152             $errors['count'] = $this->getRows();
1153         }
1154         if ($this->getError()) {
1155             $errors['error'] = $this->getError();
1156             $errors['ecode'] = $this->getErrno();
1157         }
1158         dump($errors);
1159     }
1160 
1161     /**
1162      * Setzt eine Spalte auf den naechst moeglich auto_increment Wert.
1163      *
1164      * @param string $field    Name der Spalte
1165      * @param int    $start_id
1166      *
1167      * @return int
1168      *
1169      * @throws rex_sql_exception
1170      */
1171     public function setNewId($field, $start_id = 0)
1172     {
1173         // setNewId muss neues sql Objekt verwenden, da sonst bestehende informationen im Objekt ueberschrieben werden
1174         $sql = self::factory();
1175         $sql->setQuery('SELECT ' . $this->escapeIdentifier($field) . ' FROM ' . $this->escapeIdentifier($this->table) . ' ORDER BY ' . $this->escapeIdentifier($field) . ' DESC LIMIT 1');
1176         if ($sql->getRows() == 0) {
1177             $id = $start_id;
1178         } else {
1179             $id = $sql->getValue($field);
1180         }
1181         ++$id;
1182         $this->setValue($field, $id);
1183 
1184         return $id;
1185     }
1186 
1187     /**
1188      * Gibt die Spaltennamen des ResultSets zurueck.
1189      *
1190      * @return null|array
1191      */
1192     public function getFieldnames()
1193     {
1194         $this->fetchMeta();
1195         return $this->fieldnames;
1196     }
1197 
1198     /**
1199      * @return null|array
1200      */
1201     public function getTablenames()
1202     {
1203         $this->fetchMeta();
1204         return $this->tablenames;
1205     }
1206 
1207     private function fetchMeta()
1208     {
1209         if ($this->fieldnames === null) {
1210             $this->rawFieldnames = [];
1211             $this->fieldnames = [];
1212             $this->tablenames = [];
1213 
1214             for ($i = 0; $i < $this->getFields(); ++$i) {
1215                 $metadata = $this->stmt->getColumnMeta($i);
1216 
1217                 // strip table-name from column
1218                 $this->fieldnames[] = substr($metadata['name'], strlen($metadata['table'] . '.'));
1219                 $this->rawFieldnames[] = $metadata['name'];
1220 
1221                 if (!in_array($metadata['table'], $this->tablenames)) {
1222                     $this->tablenames[] = $metadata['table'];
1223                 }
1224             }
1225         }
1226     }
1227 
1228     /**
1229      * Escaped den uebergeben Wert fuer den DB Query.
1230      *
1231      * @param string $value den zu escapenden Wert
1232      *
1233      * @return string
1234      */
1235     public function escape($value)
1236     {
1237         return self::$pdo[$this->DBID]->quote($value);
1238     }
1239 
1240     /**
1241      * Escapes and adds backsticks around.
1242      *
1243      * @param string $name
1244      *
1245      * @return string
1246      */
1247     public function escapeIdentifier($name)
1248     {
1249         return '`' . str_replace('`', '``', $name) . '`';
1250     }
1251 
1252     /**
1253      * @param string $user the name of the user who created the dataset. Defaults to the current user
1254      *
1255      * @return $this the current rex_sql object
1256      */
1257     public function addGlobalUpdateFields($user = null)
1258     {
1259         if (!$user) {
1260             if (rex::getUser()) {
1261                 $user = rex::getUser()->getValue('login');
1262             } else {
1263                 $user = rex::getEnvironment();
1264             }
1265         }
1266 
1267         $this->setDateTimeValue('updatedate', time());
1268         $this->setValue('updateuser', $user);
1269 
1270         return $this;
1271     }
1272 
1273     /**
1274      * @param string $user the name of the user who updated the dataset. Defaults to the current user
1275      *
1276      * @return $this the current rex_sql object
1277      */
1278     public function addGlobalCreateFields($user = null)
1279     {
1280         if (!$user) {
1281             if (rex::getUser()) {
1282                 $user = rex::getUser()->getValue('login');
1283             } else {
1284                 $user = rex::getEnvironment();
1285             }
1286         }
1287 
1288         $this->setDateTimeValue('createdate', time());
1289         $this->setValue('createuser', $user);
1290 
1291         return $this;
1292     }
1293 
1294     /**
1295      * Starts a database transaction.
1296      *
1297      * @throws rex_sql_exception when a transaction is already running
1298      *
1299      * @return bool Indicating whether the transaction was successfully started
1300      */
1301     public function beginTransaction()
1302     {
1303         if (self::$pdo[$this->DBID]->inTransaction()) {
1304             throw new rex_sql_exception('Transaction already started', null, $this);
1305         }
1306         return self::$pdo[$this->DBID]->beginTransaction();
1307     }
1308 
1309     /**
1310      * Rollback a already started database transaction.
1311      *
1312      * @throws rex_sql_exception when no transaction was started beforehand
1313      *
1314      * @return bool Indicating whether the transaction was successfully rollbacked
1315      */
1316     public function rollBack()
1317     {
1318         if (!self::$pdo[$this->DBID]->inTransaction()) {
1319             throw new rex_sql_exception('Unable to rollback, no transaction started before', null, $this);
1320         }
1321         return self::$pdo[$this->DBID]->rollBack();
1322     }
1323 
1324     /**
1325      * Commit a already started database transaction.
1326      *
1327      * @throws rex_sql_exception when no transaction was started beforehand
1328      *
1329      * @return bool Indicating whether the transaction was successfully committed
1330      */
1331     public function commit()
1332     {
1333         if (!self::$pdo[$this->DBID]->inTransaction()) {
1334             throw new rex_sql_exception('Unable to commit, no transaction started before', null, $this);
1335         }
1336         return self::$pdo[$this->DBID]->commit();
1337     }
1338 
1339     /**
1340      * @return bool Whether a transaction was already started/is already running.
1341      */
1342     public function inTransaction()
1343     {
1344         return self::$pdo[$this->DBID]->inTransaction();
1345     }
1346 
1347     /**
1348      * Convenience method which executes the given callable within a transaction.
1349      *
1350      * In case the callable throws, the transaction will automatically rolled back.
1351      * In case no error happens, the transaction will be committed after the callable was called.
1352      *
1353      * @param callable $callable
1354      *
1355      * @return mixed
1356      *
1357      * @throws Throwable
1358      */
1359     public function transactional(callable $callable)
1360     {
1361         $inTransaction = self::$pdo[$this->DBID]->inTransaction();
1362         if (!$inTransaction) {
1363             self::$pdo[$this->DBID]->beginTransaction();
1364         }
1365         try {
1366             $result = $callable();
1367             if (!$inTransaction) {
1368                 self::$pdo[$this->DBID]->commit();
1369             }
1370             return $result;
1371         } catch (\Exception $e) {
1372             if (!$inTransaction) {
1373                 self::$pdo[$this->DBID]->rollBack();
1374             }
1375             throw $e;
1376         } catch (\Throwable $e) {
1377             if (!$inTransaction) {
1378                 self::$pdo[$this->DBID]->rollBack();
1379             }
1380             throw $e;
1381         }
1382     }
1383 
1384     // ----------------- iterator interface
1385 
1386     /**
1387      * @see http://www.php.net/manual/en/iterator.rewind.php
1388      *
1389      * @throws rex_sql_exception
1390      */
1391     public function rewind()
1392     {
1393         $this->reset();
1394     }
1395 
1396     /**
1397      * @see http://www.php.net/manual/en/iterator.current.php
1398      *
1399      * @return $this
1400      */
1401     public function current()
1402     {
1403         return $this;
1404     }
1405 
1406     /**
1407      * @see http://www.php.net/manual/en/iterator.key.php
1408      */
1409     public function key()
1410     {
1411         return $this->counter;
1412     }
1413 
1414     /**
1415      * @see http://www.php.net/manual/en/iterator.next.php
1416      */
1417     public function next()
1418     {
1419         ++$this->counter;
1420         $this->lastRow = null;
1421     }
1422 
1423     /**
1424      * @see http://www.php.net/manual/en/iterator.valid.php
1425      */
1426     public function valid()
1427     {
1428         return $this->hasNext();
1429     }
1430 
1431     // ----------------- /iterator interface
1432 
1433     /**
1434      * Erstellt das CREATE TABLE Statement um die Tabelle $table
1435      * der Datenbankverbindung $DBID zu erstellen.
1436      *
1437      * @param string $table Name der Tabelle
1438      * @param int    $DBID  Id der Datenbankverbindung
1439      *
1440      * @return string CREATE TABLE Sql-Statement zu erstsellung der Tabelle
1441      *
1442      * @throws rex_sql_exception
1443      */
1444     public static function showCreateTable($table, $DBID = 1)
1445     {
1446         $sql = self::factory($DBID);
1447         $sql->setQuery('SHOW CREATE TABLE ' . $sql->escapeIdentifier($table));
1448 
1449         if (!$sql->getRows()) {
1450             throw new rex_sql_exception(sprintf('Table "%s" does not exist.', $table));
1451         }
1452         if (!$sql->hasValue('Create Table')) {
1453             throw new rex_sql_exception(sprintf('Table "%s" does not exist, it is a view instead.', $table));
1454         }
1455 
1456         return $sql->getValue('Create Table');
1457     }
1458 
1459     /**
1460      * Sucht alle Tabellen/Views der Datenbankverbindung $DBID.
1461      * Falls $tablePrefix gesetzt ist, werden nur dem Prefix entsprechende Tabellen gesucht.
1462      *
1463      * @param int         $DBID        Id der Datenbankverbindung
1464      * @param null|string $tablePrefix Zu suchender Tabellennamen-Prefix
1465      *
1466      * @return array Ein Array von Tabellennamen
1467      *
1468      * @throws rex_sql_exception
1469      *
1470      * @deprecated since 5.6.2, use non-static getTablesAndViews instead.
1471      */
1472     public static function showTables($DBID = 1, $tablePrefix = null)
1473     {
1474         return self::factory($DBID)->getTablesAndViews($tablePrefix);
1475     }
1476 
1477     /**
1478      * Sucht alle Tabellen/Views der Datenbankverbindung $DBID.
1479      * Falls $tablePrefix gesetzt ist, werden nur dem Prefix entsprechende Tabellen gesucht.
1480      *
1481      * @param null|string $tablePrefix Zu suchender Tabellennamen-Prefix
1482      *
1483      * @return array Ein Array von Tabellennamen
1484      *
1485      * @throws rex_sql_exception
1486      */
1487     public function getTablesAndViews($tablePrefix = null)
1488     {
1489         return $this->fetchTablesAndViews($tablePrefix);
1490     }
1491 
1492     /**
1493      * Sucht alle Tabellen der Datenbankverbindung $DBID.
1494      * Falls $tablePrefix gesetzt ist, werden nur dem Prefix entsprechende Tabellen gesucht.
1495      *
1496      * @param null|string $tablePrefix Zu suchender Tabellennamen-Prefix
1497      *
1498      * @return array Ein Array von Tabellennamen
1499      *
1500      * @throws rex_sql_exception
1501      */
1502     public function getTables($tablePrefix = null)
1503     {
1504         return $this->fetchTablesAndViews($tablePrefix, 'Table_type = "BASE TABLE"');
1505     }
1506 
1507     /**
1508      * Sucht alle Views der Datenbankverbindung $DBID.
1509      * Falls $tablePrefix gesetzt ist, werden nur dem Prefix entsprechende Views gesucht.
1510      *
1511      * @param null|string $tablePrefix Zu suchender Tabellennamen-Prefix
1512      *
1513      * @return array Ein Array von Viewnamen
1514      *
1515      * @throws rex_sql_exception
1516      */
1517     public function getViews($tablePrefix = null)
1518     {
1519         return $this->fetchTablesAndViews($tablePrefix, 'Table_type = "VIEW"');
1520     }
1521 
1522     /**
1523      * @param null|string $tablePrefix
1524      * @param null|string $where
1525      *
1526      * @return array
1527      *
1528      * @throws rex_sql_exception
1529      */
1530     private function fetchTablesAndViews($tablePrefix = null, $where = null)
1531     {
1532         $qry = 'SHOW FULL TABLES';
1533 
1534         $where = $where ? [$where] : [];
1535 
1536         if ($tablePrefix != null) {
1537             // replace LIKE wildcards
1538             $tablePrefix = str_replace(['_', '%'], ['\_', '\%'], $tablePrefix);
1539             $column = $this->escapeIdentifier('Tables_in_'.rex::getProperty('db')[$this->DBID]['name']);
1540             $where[] = $column.' LIKE "' . $tablePrefix . '%"';
1541         }
1542 
1543         if ($where) {
1544             $qry .= ' WHERE '.implode(' AND ', $where);
1545         }
1546 
1547         $tables = $this->getArray($qry);
1548         $tables = array_map('reset', $tables);
1549 
1550         return $tables;
1551     }
1552 
1553     /**
1554      * Sucht Spalteninformationen der Tabelle $table der Datenbankverbindung $DBID.
1555      *
1556      * Beispiel fuer den Rueckgabewert:
1557      *
1558      * Array (
1559      *  [0] => Array (
1560      *    [name] => pid
1561      *    [type] => int(11)
1562      *    [null] => NO
1563      *    [key] => PRI
1564      *    [default] =>
1565      *    [extra] => auto_increment
1566      *  )
1567      *  [1] => Array (
1568      *    [name] => id
1569      *    [type] => int(11)
1570      *    [null] => NO
1571      *    [key] => MUL
1572      *    [default] =>
1573      *    [extra] =>
1574      *  )
1575      * )
1576      *
1577      * @param string $table Name der Tabelle
1578      * @param int    $DBID  Id der Datenbankverbindung
1579      *
1580      * @return array Ein mehrdimensionales Array das die Metadaten enthaelt
1581      *
1582      * @throws rex_sql_exception
1583      */
1584     public static function showColumns($table, $DBID = 1)
1585     {
1586         $sql = self::factory($DBID);
1587         $sql->setQuery('SHOW COLUMNS FROM ' . $sql->escapeIdentifier($table));
1588 
1589         $columns = [];
1590         foreach ($sql as $col) {
1591             $columns[] = [
1592                 'name' => $col->getValue('Field'),
1593                 'type' => $col->getValue('Type'),
1594                 'null' => $col->getValue('Null'),
1595                 'key' => $col->getValue('Key'),
1596                 'default' => $col->getValue('Default'),
1597                 'extra' => $col->getValue('Extra'),
1598             ];
1599         }
1600 
1601         return $columns;
1602     }
1603 
1604     /**
1605      * Gibt die Serverversion zurueck.
1606      *
1607      * Die Versionsinformation ist erst bekannt,
1608      * nachdem der rex_sql Konstruktor einmalig erfolgreich durchlaufen wurde.
1609      *
1610      * @param int $DBID
1611      *
1612      * @return mixed
1613      */
1614     public static function getServerVersion($DBID = 1)
1615     {
1616         if (!isset(self::$pdo[$DBID])) {
1617             // create connection if necessary
1618             self::factory($DBID);
1619         }
1620         return self::$pdo[$DBID]->getAttribute(PDO::ATTR_SERVER_VERSION);
1621     }
1622 
1623     /**
1624      * Creates a rex_sql instance.
1625      *
1626      * @param int $DBID
1627      *
1628      * @return static Returns a rex_sql instance
1629      */
1630     public static function factory($DBID = 1)
1631     {
1632         $class = static::getFactoryClass();
1633         return new $class($DBID);
1634     }
1635 
1636     /**
1637      * Prueft die uebergebenen Zugangsdaten auf gueltigkeit und legt ggf. die
1638      * Datenbank an.
1639      *
1640      * @param string $host
1641      * @param string $login
1642      * @param string $pw
1643      * @param string $dbname
1644      * @param bool   $createDb
1645      *
1646      * @return bool|string
1647      */
1648     public static function checkDbConnection($host, $login, $pw, $dbname, $createDb = false)
1649     {
1650         if (!$dbname) {
1651             return rex_i18n::msg('sql_database_name_missing');
1652         }
1653 
1654         $err_msg = true;
1655 
1656         try {
1657             self::createConnection(
1658                 $host,
1659                 $dbname,
1660                 $login,
1661                 $pw
1662             );
1663 
1664             // db connection was successfully established, but we were meant to create the db
1665             if ($createDb) {
1666                 // -> throw db already exists error
1667                 $err_msg = rex_i18n::msg('sql_database_already_exists');
1668             }
1669         } catch (PDOException $e) {
1670             // see mysql error codes at http://dev.mysql.com/doc/refman/5.1/de/error-messages-server.html
1671 
1672             // ER_BAD_HOST
1673             if (strpos($e->getMessage(), 'SQLSTATE[HY000] [2002]') !== false) {
1674                 // unable to connect to db server
1675                 $err_msg = rex_i18n::msg('sql_unable_to_connect_database');
1676             }
1677             // ER_BAD_DB_ERROR
1678             elseif (strpos($e->getMessage(), 'SQLSTATE[HY000] [1049]') !== false ||
1679                     strpos($e->getMessage(), 'SQLSTATE[42000]') !== false
1680             ) {
1681                 if ($createDb) {
1682                     try {
1683                         // use the "mysql" db for the connection
1684                         $conn = self::createConnection(
1685                             $host,
1686                             'mysql',
1687                             $login,
1688                             $pw
1689                         );
1690                         if ($conn->exec('CREATE DATABASE ' . $dbname . ' CHARACTER SET utf8 COLLATE utf8_general_ci') !== 1) {
1691                             // unable to create db
1692                             $err_msg = rex_i18n::msg('sql_unable_to_create_database');
1693                         }
1694                     } catch (PDOException $e) {
1695                         // unable to find database
1696                         $err_msg = rex_i18n::msg('sql_unable_to_open_database');
1697                     }
1698                 } else {
1699                     // unable to find database
1700                     $err_msg = rex_i18n::msg('sql_unable_to_find_database');
1701                 }
1702             }
1703             // ER_ACCESS_DENIED_ERROR
1704             // ER_DBACCESS_DENIED_ERROR
1705             elseif (
1706                 strpos($e->getMessage(), 'SQLSTATE[HY000] [1045]') !== false ||
1707                 strpos($e->getMessage(), 'SQLSTATE[28000]') !== false ||
1708                 strpos($e->getMessage(), 'SQLSTATE[HY000] [1044]') !== false ||
1709                 strpos($e->getMessage(), 'SQLSTATE[42000]') !== false
1710             ) {
1711                 // unable to connect to db
1712                 $err_msg = rex_i18n::msg('sql_unable_to_connect_database');
1713             }
1714             // ER_ACCESS_TO_SERVER_ERROR
1715             elseif (
1716                 strpos($e->getMessage(), 'SQLSTATE[HY000] [2005]') !== false
1717             ) {
1718                 // unable to connect to server
1719                 $err_msg = rex_i18n::msg('sql_unable_to_connect_server');
1720             } else {
1721                 // we didn't expected this error, so rethrow it to show it to the admin/end-user
1722                 throw $e;
1723             }
1724         }
1725 
1726         // close the connection
1727         $conn = null;
1728 
1729         return  $err_msg;
1730     }
1731 
1732     /**
1733      * @param string $verb
1734      * @param bool   $onDuplicateKeyUpdate
1735      *
1736      * @return $this|rex_sql
1737      *
1738      * @throws rex_sql_exception
1739      */
1740     private function setMultiRecordQuery($verb, $onDuplicateKeyUpdate = false)
1741     {
1742         $fields = [];
1743 
1744         foreach ($this->records as $record) {
1745             foreach ($record->values as $field => $value) {
1746                 $fields[$field] = true;
1747             }
1748             foreach ($record->rawValues as $field => $value) {
1749                 $fields[$field] = true;
1750             }
1751         }
1752 
1753         $fields = array_keys($fields);
1754 
1755         $rows = [];
1756         $params = [];
1757 
1758         foreach ($this->records as $record) {
1759             $row = [];
1760 
1761             foreach ($fields as $field) {
1762                 if (isset($record->rawValues[$field])) {
1763                     $row[] = $record->rawValues[$field];
1764 
1765                     continue;
1766                 }
1767 
1768                 if (!isset($record->values[$field]) && !array_key_exists($field, $this->values)) {
1769                     $row[] = 'DEFAULT';
1770 
1771                     continue;
1772                 }
1773 
1774                 $row[] = '?';
1775                 $params[] = $record->values[$field];
1776             }
1777 
1778             $rows[] = '('.implode(', ', $row).')';
1779         }
1780 
1781         $query = $verb.' INTO '.$this->escapeIdentifier($this->table)."\n";
1782         $query .= '('.implode(', ', array_map([$this, 'escapeIdentifier'], $fields)).")\n";
1783         $query .= "VALUES\n";
1784         $query .= implode(",\n", $rows);
1785 
1786         if ($onDuplicateKeyUpdate) {
1787             $query .= "\n".$this->buildOnDuplicateKeyUpdate($fields);
1788         }
1789 
1790         return $this->setQuery($query, $params);
1791     }
1792 
1793     /**
1794      * @param array $fields
1795      *
1796      * @return string
1797      */
1798     private function buildOnDuplicateKeyUpdate($fields)
1799     {
1800         $updates = [];
1801 
1802         foreach ($fields as $field) {
1803             $field = $this->escapeIdentifier($field);
1804             $updates[] = "$field = VALUES($field)";
1805         }
1806 
1807         return 'ON DUPLICATE KEY UPDATE '.implode(', ', $updates);
1808     }
1809 }
1810