summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Data/Db
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--library/Icinga/Data/Db/DbConnection.php655
-rw-r--r--library/Icinga/Data/Db/DbQuery.php565
2 files changed, 1220 insertions, 0 deletions
diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php
new file mode 100644
index 0000000..fc6814d
--- /dev/null
+++ b/library/Icinga/Data/Db/DbConnection.php
@@ -0,0 +1,655 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Db;
+
+use DateTime;
+use DateTimeZone;
+use Exception;
+use Icinga\Data\Filter\FilterEqual;
+use Icinga\Data\Filter\FilterNotEqual;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use PDO;
+use Iterator;
+use Zend_Db;
+use Zend_Db_Expr;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Extensible;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Data\Filter\FilterNot;
+use Icinga\Data\Filter\FilterOr;
+use Icinga\Data\Reducible;
+use Icinga\Data\ResourceFactory;
+use Icinga\Data\Selectable;
+use Icinga\Data\Updatable;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\ProgrammingError;
+
+/**
+ * Encapsulate database connections and query creation
+ */
+class DbConnection implements Selectable, Extensible, Updatable, Reducible, Inspectable
+{
+ /**
+ * Connection config
+ *
+ * @var ConfigObject
+ */
+ private $config;
+
+ /**
+ * Database type
+ *
+ * @var string
+ */
+ private $dbType;
+
+ /**
+ * @var \Zend_Db_Adapter_Abstract
+ */
+ private $dbAdapter;
+
+ /**
+ * Table prefix
+ *
+ * @var string
+ */
+ private $tablePrefix = '';
+
+ private static $genericAdapterOptions = array(
+ Zend_Db::AUTO_QUOTE_IDENTIFIERS => false,
+ Zend_Db::CASE_FOLDING => Zend_Db::CASE_LOWER
+ );
+
+ private static $driverOptions = array(
+ PDO::ATTR_TIMEOUT => 10,
+ PDO::ATTR_CASE => PDO::CASE_LOWER,
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
+ );
+
+ /**
+ * Create a new connection object
+ *
+ * @param ConfigObject $config
+ */
+ public function __construct(ConfigObject $config = null)
+ {
+ $this->config = $config;
+ $this->connect();
+ }
+
+ /**
+ * Provide a query on this connection
+ *
+ * @return DbQuery
+ */
+ public function select()
+ {
+ return new DbQuery($this);
+ }
+
+ /**
+ * Fetch and return all rows of the given query's result set using an iterator
+ *
+ * @param DbQuery $query
+ *
+ * @return Iterator
+ */
+ public function query(DbQuery $query)
+ {
+ return $query->getSelectQuery()->query();
+ }
+
+ /**
+ * Get the connection configuration
+ *
+ * @return ConfigObject
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Getter for database type
+ *
+ * @return string
+ */
+ public function getDbType()
+ {
+ return $this->dbType;
+ }
+
+ /**
+ * Getter for the Zend_Db_Adapter
+ *
+ * @return \Zend_Db_Adapter_Abstract
+ */
+ public function getDbAdapter()
+ {
+ return $this->dbAdapter;
+ }
+
+ /**
+ * Create a new connection
+ */
+ private function connect()
+ {
+ $genericAdapterOptions = self::$genericAdapterOptions;
+ $driverOptions = self::$driverOptions;
+ $adapterParamaters = array(
+ 'host' => $this->config->host,
+ 'username' => $this->config->username,
+ 'password' => $this->config->password,
+ 'dbname' => $this->config->dbname,
+ 'charset' => $this->config->charset ?: null,
+ 'options' => & $genericAdapterOptions,
+ 'driver_options' => & $driverOptions
+ );
+ $this->dbType = strtolower($this->config->get('db', 'mysql'));
+ switch ($this->dbType) {
+ case 'mssql':
+ $adapter = 'Pdo_Mssql';
+ $pdoType = $this->config->get('pdoType');
+ if (empty($pdoType)) {
+ if (extension_loaded('sqlsrv')) {
+ $adapter = 'Sqlsrv';
+ } else {
+ $pdoType = 'dblib';
+ }
+ }
+ if ($pdoType === 'dblib') {
+ // Driver does not support setting attributes
+ unset($adapterParamaters['options']);
+ unset($adapterParamaters['driver_options']);
+ }
+ if (! empty($pdoType)) {
+ $adapterParamaters['pdoType'] = $pdoType;
+ }
+ $defaultPort = 1433;
+ break;
+ case 'mysql':
+ $adapter = 'Pdo_Mysql';
+ if ($this->config->use_ssl) {
+ # The presence of these keys as empty strings or null cause non-ssl connections to fail
+ if ($this->config->ssl_key) {
+ $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_KEY] = $this->config->ssl_key;
+ }
+ if ($this->config->ssl_cert) {
+ $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CERT] = $this->config->ssl_cert;
+ }
+ if ($this->config->ssl_ca) {
+ $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CA] = $this->config->ssl_ca;
+ }
+ if ($this->config->ssl_capath) {
+ $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CAPATH] = $this->config->ssl_capath;
+ }
+ if ($this->config->ssl_cipher) {
+ $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CIPHER] = $this->config->ssl_cipher;
+ }
+ if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')
+ && $this->config->ssl_do_not_verify_server_cert
+ ) {
+ $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false;
+ }
+ }
+ /*
+ * Set MySQL server SQL modes to behave as closely as possible to Oracle and PostgreSQL. Note that the
+ * ONLY_FULL_GROUP_BY mode is left on purpose because MySQL requires you to specify all non-aggregate
+ * columns in the group by list even if the query is grouped by the master table's primary key which is
+ * valid ANSI SQL though. Further in that case the query plan would suffer if you add more columns to
+ * the group by list.
+ */
+ $driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] =
+ 'SET SESSION SQL_MODE=\'STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,'
+ . 'ANSI_QUOTES,PIPES_AS_CONCAT,NO_ENGINE_SUBSTITUTION\'';
+ if (isset($adapterParamaters['charset'])) {
+ $driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .= ', NAMES ' . $adapterParamaters['charset'];
+ if (trim($adapterParamaters['charset']) === 'latin1') {
+ // Required for MySQL 8+ because we need PIPES_AS_CONCAT and
+ // have several columns with explicit COLLATE instructions
+ $driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .= ' COLLATE latin1_general_ci';
+ }
+
+ unset($adapterParamaters['charset']);
+ }
+
+ $driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .= ", time_zone='" . $this->defaultTimezoneOffset() . "'";
+ $driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .=';';
+ $defaultPort = 3306;
+ break;
+ case 'oci':
+ $adapter = 'Oracle';
+ unset($adapterParamaters['options']);
+ unset($adapterParamaters['driver_options']);
+ $adapterParamaters['driver_options'] = array(
+ 'lob_as_string' => true
+ );
+ $defaultPort = 1521;
+ break;
+ case 'oracle':
+ $adapter = 'Pdo_Oci';
+ $defaultPort = 1521;
+
+ // remove host parameter when not configured
+ if (empty($this->config->host)) {
+ unset($adapterParamaters['host']);
+ }
+ break;
+ case 'pgsql':
+ $adapter = 'Pdo_Pgsql';
+ $defaultPort = 5432;
+ break;
+ case 'ibm':
+ $adapter = 'Pdo_Ibm';
+ $defaultPort = 50000;
+ break;
+ case 'sqlite':
+ $adapter = 'Pdo_Sqlite';
+ $defaultPort = 0; // Dummy port because a value is required
+ break;
+ default:
+ throw new ConfigurationError(
+ 'Backend "%s" is not supported',
+ $this->dbType
+ );
+ }
+ $adapterParamaters['port'] = $this->config->get('port', $defaultPort);
+ $this->dbAdapter = Zend_Db::factory($adapter, $adapterParamaters);
+ $this->dbAdapter->setFetchMode(Zend_Db::FETCH_OBJ);
+ // TODO(el/tg): The profiler is disabled per default, why do we disable the profiler explicitly?
+ $this->dbAdapter->getProfiler()->setEnabled(false);
+ }
+
+ public static function fromResourceName($name)
+ {
+ return new static(ResourceFactory::getResourceConfig($name));
+ }
+
+ /**
+ * Getter for the table prefix
+ *
+ * @return string
+ */
+ public function getTablePrefix()
+ {
+ return $this->tablePrefix;
+ }
+
+ /**
+ * Setter for the table prefix
+ *
+ * @param string $prefix
+ *
+ * @return $this
+ */
+ public function setTablePrefix($prefix)
+ {
+ $this->tablePrefix = $prefix;
+ return $this;
+ }
+
+ /**
+ * Get offset from the current default timezone to GMT
+ *
+ * @return string
+ */
+ protected function defaultTimezoneOffset()
+ {
+ $tz = new DateTimeZone(date_default_timezone_get());
+ $offset = $tz->getOffset(new DateTime());
+ $prefix = $offset >= 0 ? '+' : '-';
+ $offset = abs($offset);
+ $hours = (int) floor($offset / 3600);
+ $minutes = (int) floor(($offset % 3600) / 60);
+ return sprintf('%s%d:%02d', $prefix, $hours, $minutes);
+ }
+
+ /**
+ * Count all rows of the result set
+ *
+ * @param DbQuery $query
+ *
+ * @return int
+ */
+ public function count(DbQuery $query)
+ {
+ return (int) $this->dbAdapter->fetchOne($query->getCountQuery());
+ }
+
+ /**
+ * Retrieve an array containing all rows of the result set
+ *
+ * @param DbQuery $query
+ *
+ * @return array
+ */
+ public function fetchAll(DbQuery $query)
+ {
+ return $this->dbAdapter->fetchAll($query->getSelectQuery());
+ }
+
+ /**
+ * Fetch the first row of the result set
+ *
+ * @param DbQuery $query
+ *
+ * @return mixed
+ */
+ public function fetchRow(DbQuery $query)
+ {
+ return $this->dbAdapter->fetchRow($query->getSelectQuery());
+ }
+
+ /**
+ * Fetch the first column of all rows of the result set as an array
+ *
+ * @param DbQuery $query
+ *
+ * @return array
+ */
+ public function fetchColumn(DbQuery $query)
+ {
+ return $this->dbAdapter->fetchCol($query->getSelectQuery());
+ }
+
+ /**
+ * Fetch the first column of the first row of the result set
+ *
+ * @param DbQuery $query
+ *
+ * @return string
+ */
+ public function fetchOne(DbQuery $query)
+ {
+ return $this->dbAdapter->fetchOne($query->getSelectQuery());
+ }
+
+ /**
+ * Fetch all rows of the result set as an array of key-value pairs
+ *
+ * The first column is the key, the second column is the value.
+ *
+ * @param DbQuery $query
+ *
+ * @return array
+ */
+ public function fetchPairs(DbQuery $query)
+ {
+ return $this->dbAdapter->fetchPairs($query->getSelectQuery());
+ }
+
+ /**
+ * Insert a table row with the given data
+ *
+ * Note that the base implementation does not perform any quoting on the $table argument.
+ * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
+ * as third parameter $types to define a different type than string for a particular column.
+ *
+ * @param string $table
+ * @param array $bind
+ * @param array $types
+ *
+ * @return int The number of affected rows
+ */
+ public function insert($table, array $bind, array $types = array())
+ {
+ $columns = $values = array();
+ foreach ($bind as $column => $value) {
+ $columns[] = $column;
+ if ($value instanceof Zend_Db_Expr) {
+ $values[] = (string) $value;
+ unset($bind[$column]);
+ } else {
+ $values[] = ':' . $column;
+ }
+ }
+
+ $sql = 'INSERT INTO ' . $table
+ . ' (' . join(', ', $columns) . ') '
+ . 'VALUES (' . join(', ', $values) . ')';
+ $statement = $this->dbAdapter->prepare($sql);
+
+ foreach ($bind as $column => $value) {
+ $type = isset($types[$column]) ? $types[$column] : PDO::PARAM_STR;
+ $statement->bindValue(':' . $column, $value, $type);
+ }
+
+ $statement->execute();
+ return $statement->rowCount();
+ }
+
+ /**
+ * Update table rows with the given data, optionally limited by using a filter
+ *
+ * Note that the base implementation does not perform any quoting on the $table argument.
+ * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
+ * as fourth parameter $types to define a different type than string for a particular column.
+ *
+ * @param string $table
+ * @param array $bind
+ * @param Filter $filter
+ * @param array $types
+ *
+ * @return int The number of affected rows
+ */
+ public function update($table, array $bind, Filter $filter = null, array $types = array())
+ {
+ $set = array();
+ foreach ($bind as $column => $value) {
+ if ($value instanceof Zend_Db_Expr) {
+ $set[] = $column . ' = ' . $value;
+ unset($bind[$column]);
+ } else {
+ $set[] = $column . ' = :' . $column;
+ }
+ }
+
+ $sql = 'UPDATE ' . $table
+ . ' SET ' . join(', ', $set)
+ . ($filter ? ' WHERE ' . $this->renderFilter($filter) : '');
+ $statement = $this->dbAdapter->prepare($sql);
+
+ foreach ($bind as $column => $value) {
+ $type = isset($types[$column]) ? $types[$column] : PDO::PARAM_STR;
+ $statement->bindValue(':' . $column, $value, $type);
+ }
+
+ $statement->execute();
+ return $statement->rowCount();
+ }
+
+ /**
+ * Delete table rows, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param Filter $filter
+ *
+ * @return int The number of affected rows
+ */
+ public function delete($table, Filter $filter = null)
+ {
+ return $this->dbAdapter->delete($table, $filter ? $this->renderFilter($filter) : '');
+ }
+
+ /**
+ * Render and return the given filter as SQL-WHERE clause
+ *
+ * @param Filter $filter
+ *
+ * @return string
+ */
+ public function renderFilter(Filter $filter, $level = 0)
+ {
+ // TODO: This is supposed to supersede DbQuery::renderFilter()
+ $where = '';
+ if ($filter->isChain()) {
+ if ($filter instanceof FilterAnd) {
+ $operator = ' AND ';
+ } elseif ($filter instanceof FilterOr) {
+ $operator = ' OR ';
+ } elseif ($filter instanceof FilterNot) {
+ $operator = ' AND ';
+ $where .= ' NOT ';
+ } else {
+ throw new ProgrammingError('Cannot render filter: %s', get_class($filter));
+ }
+
+ if (! $filter->isEmpty()) {
+ $parts = array();
+ foreach ($filter->filters() as $filterPart) {
+ $part = $this->renderFilter($filterPart, $level + 1);
+ if ($part) {
+ $parts[] = $part;
+ }
+ }
+
+ if (! empty($parts)) {
+ if ($level > 0) {
+ $where .= ' (' . implode($operator, $parts) . ') ';
+ } else {
+ $where .= implode($operator, $parts);
+ }
+ }
+ } else {
+ return ''; // Explicitly return the empty string due to the FilterNot case
+ }
+ } else {
+ $where .= $this->renderFilterExpression($filter);
+ }
+
+ return $where;
+ }
+
+ /**
+ * Render and return the given filter expression
+ *
+ * @param Filter $filter
+ *
+ * @return string
+ */
+ protected function renderFilterExpression(Filter $filter)
+ {
+ $column = $filter->getColumn();
+ $sign = $filter->getSign();
+ $value = $filter->getExpression();
+
+ if (is_array($value)) {
+ $comp = [];
+ $pattern = [];
+ foreach ($value as $val) {
+ if (strpos($val, '*') === false) {
+ $comp[] = $val;
+ } else {
+ $pattern[] = $this->renderFilterExpression(Filter::expression($column, $sign, $val));
+ }
+ }
+
+ $sql = $pattern;
+ if ($sign === '=') {
+ if (! empty($comp)) {
+ $sql[] = $column . ' IN (' . $this->dbAdapter->quote($comp) . ')';
+ }
+
+ $operator = 'OR';
+ } elseif ($sign === '!=') {
+ if (! empty($comp)) {
+ $sql[] = sprintf(
+ '(%1$s NOT IN (%2$s) OR %1$s IS NULL)',
+ $column,
+ $this->dbAdapter->quote($comp)
+ );
+ }
+
+ $operator = 'AND';
+ } else {
+ throw new ProgrammingError(
+ 'Unable to render array expressions with operators other than equal or not equal'
+ );
+ }
+
+ return count($sql) === 1 ? $sql[0] : '(' . implode(" $operator ", $sql) . ')';
+ } elseif ($sign === '='
+ && ! $filter instanceof FilterEqual
+ && $value !== null
+ && strpos($value, '*') !== false
+ ) {
+ if ($value === '*') {
+ return $column . ' IS NOT NULL';
+ }
+
+ return $column . ' LIKE ' . $this->dbAdapter->quote(preg_replace('~\*~', '%', $value));
+ } elseif ($sign === '!='
+ && ! $filter instanceof FilterNotEqual
+ && $value !== null
+ && strpos($value, '*') !== false
+ ) {
+ if ($value === '*') {
+ return $column . ' IS NULL';
+ }
+
+ return sprintf(
+ '(%1$s NOT LIKE %2$s OR %1$s IS NULL)',
+ $column,
+ $this->dbAdapter->quote(preg_replace('~\*~', '%', $value))
+ );
+ } elseif ($sign === '!=') {
+ return sprintf('(%1$s != %2$s OR %1$s IS NULL)', $column, $this->dbAdapter->quote($value));
+ } else {
+ return sprintf('%s %s %s', $column, $sign, $this->dbAdapter->quote($value));
+ }
+ }
+
+ public function inspect()
+ {
+ $insp = new Inspection('Db Connection');
+ try {
+ $this->getDbAdapter()->getConnection();
+ $config = $this->dbAdapter->getConfig();
+ $insp->write(sprintf(
+ 'Connection to %s as %s on %s:%s successful',
+ $config['dbname'],
+ $config['username'],
+ array_key_exists('host', $config) ? $config['host'] : '(none)',
+ $config['port']
+ ));
+ switch ($this->dbType) {
+ case 'mysql':
+ $rows = $this->dbAdapter->query(
+ 'SHOW VARIABLES WHERE variable_name ' .
+ 'IN (\'version\', \'protocol_version\', \'version_compile_os\', \'have_ssl\');'
+ )->fetchAll();
+ $sqlinsp = new Inspection('MySQL');
+ $hasSsl = false;
+ foreach ($rows as $row) {
+ $sqlinsp->write($row->variable_name . ': ' . $row->value);
+ if ($row->variable_name === 'have_ssl' && $row->value === 'YES') {
+ $hasSsl = true;
+ }
+ }
+ if ($hasSsl) {
+ $ssl_rows = $this->dbAdapter->query(
+ 'SHOW STATUS WHERE variable_name ' .
+ 'IN (\'Ssl_Cipher\');'
+ )->fetchAll();
+ foreach ($ssl_rows as $ssl_row) {
+ $sqlinsp->write($ssl_row->variable_name . ': ' . $ssl_row->value);
+ }
+ }
+ $insp->write($sqlinsp);
+ break;
+ case 'pgsql':
+ $row = $this->dbAdapter->query('SELECT version();')->fetchAll();
+ $sqlinsp = new Inspection('PostgreSQL');
+ $sqlinsp->write($row[0]->version);
+ $insp->write($sqlinsp);
+ break;
+ }
+ } catch (Exception $e) {
+ return $insp->error(sprintf('Connection failed %s', $e->getMessage()));
+ }
+ return $insp;
+ }
+}
diff --git a/library/Icinga/Data/Db/DbQuery.php b/library/Icinga/Data/Db/DbQuery.php
new file mode 100644
index 0000000..ff1d131
--- /dev/null
+++ b/library/Icinga/Data/Db/DbQuery.php
@@ -0,0 +1,565 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Data\Db;
+
+use DateInterval;
+use DateTime;
+use DateTimeZone;
+use Exception;
+use Icinga\Data\Filter\Filter;
+use Zend_Db_Adapter_Abstract;
+use Zend_Db_Expr;
+use Zend_Db_Select;
+use Icinga\Application\Logger;
+use Icinga\Data\SimpleQuery;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\QueryException;
+
+/**
+ * Database query class
+ */
+class DbQuery extends SimpleQuery
+{
+ /**
+ * @var Zend_Db_Adapter_Abstract
+ */
+ protected $db;
+
+ /**
+ * Whether or not the query is a sub query
+ *
+ * Sub queries are automatically wrapped in parentheses
+ *
+ * @var bool
+ */
+ protected $isSubQuery = false;
+
+ /**
+ * Select query
+ *
+ * @var Zend_Db_Select
+ */
+ protected $select;
+
+ /**
+ * Whether to use a subquery for counting
+ *
+ * When the query is distinct or has a HAVING or GROUP BY clause this must be set to true
+ *
+ * @var bool
+ */
+ protected $useSubqueryCount = false;
+
+ /**
+ * Count query result
+ *
+ * Count queries are only executed once
+ *
+ * @var int
+ */
+ protected $count;
+
+ /**
+ * GROUP BY clauses
+ *
+ * @var string|array
+ */
+ protected $group;
+
+ protected function init()
+ {
+ $this->db = $this->ds->getDbAdapter();
+ $this->select = $this->db->select();
+ parent::init();
+ }
+
+ /**
+ * Get whether or not the query is a sub query
+ */
+ public function getIsSubQuery()
+ {
+ return $this->isSubQuery;
+ }
+
+ /**
+ * Set whether or not the query is a sub query
+ *
+ * @param bool $isSubQuery
+ *
+ * @return $this
+ */
+ public function setIsSubQuery($isSubQuery = true)
+ {
+ $this->isSubQuery = (bool) $isSubQuery;
+ return $this;
+ }
+
+ public function setUseSubqueryCount($useSubqueryCount = true)
+ {
+ $this->useSubqueryCount = $useSubqueryCount;
+ return $this;
+ }
+
+ public function from($target, array $fields = null)
+ {
+ parent::from($target, $fields);
+ $this->select->from($this->target, array());
+ return $this;
+ }
+
+ public function where($condition, $value = null)
+ {
+ // $this->count = $this->select = null;
+ return parent::where($condition, $value);
+ }
+
+ public function addFilter(Filter $filter)
+ {
+ $this->expressionsToTimestamp($filter);
+ return parent::addFilter($filter);
+ }
+
+ private function expressionsToTimestamp(Filter $filter)
+ {
+ if ($filter->isChain()) {
+ foreach ($filter->filters() as $child) {
+ $this->expressionsToTimestamp($child);
+ }
+ } elseif ($this->isTimestamp($filter->getColumn())) {
+ $filter->setExpression($this->valueToTimestamp($filter->getExpression()));
+ }
+ }
+
+ protected function dbSelect()
+ {
+ return clone $this->select;
+ }
+
+ /**
+ * Return the underlying select
+ *
+ * @return Zend_Db_Select
+ */
+ public function select()
+ {
+ return $this->select;
+ }
+
+ /**
+ * Get the select query
+ *
+ * Applies order and limit if any
+ *
+ * @return Zend_Db_Select
+ */
+ public function getSelectQuery()
+ {
+ $select = $this->dbSelect();
+ // Add order fields to select for postgres distinct queries (#6351)
+ if ($this->hasOrder()
+ && $this->getDatasource()->getDbType() === 'pgsql'
+ && $select->getPart(Zend_Db_Select::DISTINCT) === true) {
+ foreach ($this->getOrder() as $fieldAndDirection) {
+ if (array_search($fieldAndDirection[0], $this->columns, true) === false) {
+ $this->columns[] = $fieldAndDirection[0];
+ }
+ }
+ }
+
+ $group = $this->getGroup();
+ if ($group) {
+ $select->group($group);
+ }
+
+ if (! empty($this->columns)) {
+ $select->columns($this->columns);
+ }
+
+ $this->applyFilterSql($select);
+
+ if ($this->hasLimit() || $this->hasOffset()) {
+ $select->limit($this->getLimit(), $this->getOffset());
+ }
+ if ($this->hasOrder()) {
+ foreach ($this->getOrder() as $fieldAndDirection) {
+ $select->order(
+ $fieldAndDirection[0] . ' ' . $fieldAndDirection[1]
+ );
+ }
+ }
+
+ return $select;
+ }
+
+ protected function applyFilterSql($select)
+ {
+ $where = $this->getDatasource()->renderFilter($this->filter);
+ if ($where !== '') {
+ $select->where($where);
+ }
+ }
+
+ protected function escapeForSql($value)
+ {
+ // bindParam? bindValue?
+ if (is_array($value)) {
+ $ret = array();
+ foreach ($value as $val) {
+ $ret[] = $this->escapeForSql($val);
+ }
+ return implode(', ', $ret);
+ } else {
+ //if (preg_match('/^\d+$/', $value)) {
+ // return $value;
+ //} else {
+ return $this->db->quote($value);
+ //}
+ }
+ }
+
+ protected function escapeWildcards($value)
+ {
+ return preg_replace('/\*/', '%', $value);
+ }
+
+ protected function valueToTimestamp($value)
+ {
+ if (is_string($value)) {
+ if (ctype_digit($value)) {
+ $value = (int) $value;
+ } else {
+ $value = strtotime($value);
+ }
+ } elseif (! is_int($value)) {
+ $value = (int) $value;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Render the given timestamp based on the local timezone
+ *
+ * Since {@see DbConnection::defaultTimezoneOffset()} tells the database the timezone with just an offset,
+ * this will prepare the rendered value in a way that it plays fine with daylight savings.
+ *
+ * @param int $value
+ * @return string
+ */
+ protected function timestampForSql($value)
+ {
+ if ($this->getDatasource()->getDbType() === 'pgsql') {
+ // We don't tell PostgreSQL the user's timezone
+ $dateTime = (new DateTime())
+ ->setTimezone(new DateTimeZone('UTC'))
+ ->setTimestamp($value);
+ } else {
+ $dateTime = new DateTime();
+ // Get "current" offset the database will use
+ $offsetToUTC = $dateTime->getOffset();
+ // Set timezone to UTC and initialize it with the timestamp
+ $dateTime->setTimezone(new DateTimeZone('UTC'))->setTimestamp($value);
+ // Normalize every datetime based on the only offset the database knows about
+ if ($offsetToUTC >= 0) {
+ $dateTime->add(new DateInterval("PT{$offsetToUTC}S"));
+ } else {
+ $offsetToUTC = abs($offsetToUTC);
+ $dateTime->sub(new DateInterval("PT{$offsetToUTC}S"));
+ }
+ }
+
+ return $dateTime->format('Y-m-d H:i:s');
+ }
+
+ /**
+ * Check for timestamp fields
+ *
+ * TODO: This is not here to do automagic timestamp stuff. One may
+ * override this function for custom voodoo, IdoQuery right now
+ * does. IMO we need to split whereToSql functionality, however
+ * I'd prefer to wait with this unless we understood how other
+ * backends will work. We probably should also rename this
+ * function to isTimestampColumn().
+ *
+ * @param string $field Field Field name to checked
+ * @return bool Whether this field expects timestamps
+ */
+ public function isTimestamp($field)
+ {
+ return false;
+ }
+
+ /**
+ * Get the count query
+ *
+ * @return Zend_Db_Select
+ */
+ public function getCountQuery()
+ {
+ // TODO: there may be situations where we should clone the "select"
+ $count = $this->dbSelect();
+ $this->applyFilterSql($count);
+ $group = $this->getGroup();
+ if ($this->useSubqueryCount || $group) {
+ if (! empty($this->columns)) {
+ $count->columns($this->columns);
+ }
+ if ($group) {
+ $count->group($group);
+ }
+ $columns = array('cnt' => 'COUNT(*)');
+ return $this->db->select()->from($count, $columns);
+ }
+
+ $count->columns(array('cnt' => 'COUNT(*)'));
+ return $count;
+ }
+
+ /**
+ * Count all rows of the result set
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ if ($this->count === null) {
+ $this->count = parent::count();
+ }
+
+ return $this->count;
+ }
+
+ /**
+ * Return the select and count query as a textual representation
+ *
+ * @return string A string containing the select and count query, using unix style newlines as linebreaks
+ */
+ public function dump()
+ {
+ return "QUERY\n=====\n"
+ . $this->getSelectQuery()
+ . "\n\nCOUNT\n=====\n"
+ . $this->getCountQuery()
+ . "\n\n";
+ }
+
+ public function __clone()
+ {
+ parent::__clone();
+ $this->select = clone $this->select;
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString()
+ {
+ try {
+ $select = (string) $this->getSelectQuery();
+ return $this->getIsSubQuery() ? ('(' . $select . ')') : $select;
+ } catch (Exception $e) {
+ Logger::debug('Failed to render DbQuery. An error occured: %s', $e);
+ return '';
+ }
+ }
+
+ /**
+ * Add a GROUP BY clause
+ *
+ * @param string|array $group
+ *
+ * @return $this
+ */
+ public function group($group)
+ {
+ $this->group = $group;
+ return $this;
+ }
+
+ /**
+ * Return the GROUP BY clause
+ *
+ * @return string|array
+ */
+ public function getGroup()
+ {
+ return $this->group;
+ }
+
+ /**
+ * Return whether the given table has been joined
+ *
+ * @param string $table
+ *
+ * @return bool
+ */
+ public function hasJoinedTable($table)
+ {
+ $fromPart = $this->select->getPart(Zend_Db_Select::FROM);
+ if (isset($fromPart[$table])) {
+ return true;
+ }
+
+ foreach ($fromPart as $options) {
+ if ($options['tableName'] === $table && $options['joinType'] !== Zend_Db_Select::FROM) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Return the alias used for joining the given table
+ *
+ * @param string $table
+ *
+ * @return string|null null in case no alias is being used
+ *
+ * @throws ProgrammingError In case the given table has not been joined
+ */
+ public function getJoinedTableAlias($table)
+ {
+ $fromPart = $this->select->getPart(Zend_Db_Select::FROM);
+ if (isset($fromPart[$table])) {
+ if ($fromPart[$table]['joinType'] === Zend_Db_Select::FROM) {
+ throw new ProgrammingError('Table "%s" has not been joined', $table);
+ }
+
+ return; // No alias in use
+ }
+
+ foreach ($fromPart as $alias => $options) {
+ if ($options['tableName'] === $table && $options['joinType'] !== Zend_Db_Select::FROM) {
+ return $alias;
+ }
+ }
+
+ throw new ProgrammingError('Table "%s" has not been joined', $table);
+ }
+
+ /**
+ * Add an INNER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function join($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinInner($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add an INNER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinInner($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinInner($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a LEFT OUTER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinLeft($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinLeft($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a RIGHT OUTER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinRight($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinRight($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a FULL OUTER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinFull($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinFull($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a CROSS JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinCross($name, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinCross($name, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a NATURAL JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinNatural($name, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinNatural($name, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a UNION clause to the query
+ *
+ * @param array $select Select clauses for the union
+ * @param string $type Type of UNION to use
+ *
+ * @return $this
+ */
+ public function union($select = array(), $type = Zend_Db_Select::SQL_UNION)
+ {
+ $this->select->union($select, $type);
+ return $this;
+ }
+}