diff options
author | Roeland Jago Douma <rullzer@owncloud.com> | 2016-04-15 19:46:34 +0200 |
---|---|---|
committer | Roeland Jago Douma <rullzer@owncloud.com> | 2016-04-15 19:46:34 +0200 |
commit | d09f835dcac66347204ada3def3b8ea9d837f0ab (patch) | |
tree | 2cf5d5de1c1ef98313b80cc0251945926bfa0302 /lib/private/DB | |
parent | bb71fd6d02fd829542277129e022ef4dae840e1d (diff) | |
download | nextcloud-server-d09f835dcac66347204ada3def3b8ea9d837f0ab.tar.gz nextcloud-server-d09f835dcac66347204ada3def3b8ea9d837f0ab.zip |
Move \OC\DB to PSR-4
Besides the statement wrapper that is moved to the legacy folder
(namepsace of shame folder)
Diffstat (limited to 'lib/private/DB')
30 files changed, 4451 insertions, 0 deletions
diff --git a/lib/private/DB/Adapter.php b/lib/private/DB/Adapter.php new file mode 100644 index 00000000000..9522f768c88 --- /dev/null +++ b/lib/private/DB/Adapter.php @@ -0,0 +1,95 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Jonny007-MKD <1-23-4-5@web.de> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + +/** + * This handles the way we use to write queries, into something that can be + * handled by the database abstraction layer. + */ +class Adapter { + + /** + * @var \OC\DB\Connection $conn + */ + protected $conn; + + public function __construct($conn) { + $this->conn = $conn; + } + + /** + * @param string $table name + * @return int id of last insert statement + */ + public function lastInsertId($table) { + return $this->conn->realLastInsertId($table); + } + + /** + * @param string $statement that needs to be changed so the db can handle it + * @return string changed statement + */ + public function fixupStatement($statement) { + return $statement; + } + + /** + * Insert a row if the matching row does not exists. + * + * @param string $table The table name (will replace *PREFIX* with the actual prefix) + * @param array $input data that should be inserted into the table (column name => value) + * @param array|null $compare List of values that should be checked for "if not exists" + * If this is null or an empty array, all keys of $input will be compared + * Please note: text fields (clob) must not be used in the compare array + * @return int number of inserted rows + * @throws \Doctrine\DBAL\DBALException + */ + public function insertIfNotExist($table, $input, array $compare = null) { + if (empty($compare)) { + $compare = array_keys($input); + } + $query = 'INSERT INTO `' .$table . '` (`' + . implode('`,`', array_keys($input)) . '`) SELECT ' + . str_repeat('?,', count($input)-1).'? ' // Is there a prettier alternative? + . 'FROM `' . $table . '` WHERE '; + + $inserts = array_values($input); + foreach($compare as $key) { + $query .= '`' . $key . '`'; + if (is_null($input[$key])) { + $query .= ' IS NULL AND '; + } else { + $inserts[] = $input[$key]; + $query .= ' = ? AND '; + } + } + $query = substr($query, 0, strlen($query) - 5); + $query .= ' HAVING COUNT(*) = 0'; + + return $this->conn->executeUpdate($query, $inserts); + } +} diff --git a/lib/private/DB/AdapterMySQL.php b/lib/private/DB/AdapterMySQL.php new file mode 100644 index 00000000000..ab87c589747 --- /dev/null +++ b/lib/private/DB/AdapterMySQL.php @@ -0,0 +1,31 @@ +<?php +/** + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + + +namespace OC\DB; + +class AdapterMySQL extends Adapter { + public function fixupStatement($statement) { + $statement = str_replace(' ILIKE ', ' COLLATE utf8_general_ci LIKE ', $statement); + return $statement; + } +} diff --git a/lib/private/DB/AdapterOCI8.php b/lib/private/DB/AdapterOCI8.php new file mode 100644 index 00000000000..970d3eefa4d --- /dev/null +++ b/lib/private/DB/AdapterOCI8.php @@ -0,0 +1,50 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + + +namespace OC\DB; + +class AdapterOCI8 extends Adapter { + public function lastInsertId($table) { + if (is_null($table)) { + throw new \InvalidArgumentException('Oracle requires a table name to be passed into lastInsertId()'); + } + if ($table !== null) { + $suffix = '_SEQ'; + $table = '"' . $table . $suffix . '"'; + } + return $this->conn->realLastInsertId($table); + } + + const UNIX_TIMESTAMP_REPLACEMENT = "(cast(sys_extract_utc(systimestamp) as date) - date'1970-01-01') * 86400"; + + public function fixupStatement($statement) { + $statement = preg_replace('( LIKE \?)', '$0 ESCAPE \'\\\'', $statement); + $statement = preg_replace('/`(\w+)` ILIKE \?/', 'REGEXP_LIKE(`$1`, \'^\' || REPLACE(?, \'%\', \'.*\') || \'$\', \'i\')', $statement); + $statement = str_replace('`', '"', $statement); + $statement = str_ireplace('NOW()', 'CURRENT_TIMESTAMP', $statement); + $statement = str_ireplace('UNIX_TIMESTAMP()', self::UNIX_TIMESTAMP_REPLACEMENT, $statement); + return $statement; + } +} diff --git a/lib/private/DB/AdapterPgSql.php b/lib/private/DB/AdapterPgSql.php new file mode 100644 index 00000000000..a7d9377a0bf --- /dev/null +++ b/lib/private/DB/AdapterPgSql.php @@ -0,0 +1,37 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Morris Jobke <hey@morrisjobke.de> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + + +namespace OC\DB; + +class AdapterPgSql extends Adapter { + public function lastInsertId($table) { + return $this->conn->fetchColumn('SELECT lastval()'); + } + + const UNIX_TIMESTAMP_REPLACEMENT = 'cast(extract(epoch from current_timestamp) as integer)'; + public function fixupStatement($statement) { + $statement = str_replace( '`', '"', $statement ); + $statement = str_ireplace( 'UNIX_TIMESTAMP()', self::UNIX_TIMESTAMP_REPLACEMENT, $statement ); + return $statement; + } +} diff --git a/lib/private/DB/AdapterSqlite.php b/lib/private/DB/AdapterSqlite.php new file mode 100644 index 00000000000..d7769238abc --- /dev/null +++ b/lib/private/DB/AdapterSqlite.php @@ -0,0 +1,74 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + + +namespace OC\DB; + +class AdapterSqlite extends Adapter { + public function fixupStatement($statement) { + $statement = preg_replace('( I?LIKE \?)', '$0 ESCAPE \'\\\'', $statement); + $statement = preg_replace('/`(\w+)` ILIKE \?/', 'LOWER($1) LIKE LOWER(?)', $statement); + $statement = str_replace( '`', '"', $statement ); + $statement = str_ireplace( 'NOW()', 'datetime(\'now\')', $statement ); + $statement = str_ireplace( 'UNIX_TIMESTAMP()', 'strftime(\'%s\',\'now\')', $statement ); + return $statement; + } + + /** + * Insert a row if the matching row does not exists. + * + * @param string $table The table name (will replace *PREFIX* with the actual prefix) + * @param array $input data that should be inserted into the table (column name => value) + * @param array|null $compare List of values that should be checked for "if not exists" + * If this is null or an empty array, all keys of $input will be compared + * Please note: text fields (clob) must not be used in the compare array + * @return int number of inserted rows + * @throws \Doctrine\DBAL\DBALException + */ + public function insertIfNotExist($table, $input, array $compare = null) { + if (empty($compare)) { + $compare = array_keys($input); + } + $fieldList = '`' . implode('`,`', array_keys($input)) . '`'; + $query = "INSERT INTO `$table` ($fieldList) SELECT " + . str_repeat('?,', count($input)-1).'? ' + . " WHERE NOT EXISTS (SELECT 1 FROM `$table` WHERE "; + + $inserts = array_values($input); + foreach($compare as $key) { + $query .= '`' . $key . '`'; + if (is_null($input[$key])) { + $query .= ' IS NULL AND '; + } else { + $inserts[] = $input[$key]; + $query .= ' = ? AND '; + } + } + $query = substr($query, 0, strlen($query) - 5); + $query .= ')'; + + return $this->conn->executeUpdate($query, $inserts); + } +} diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php new file mode 100644 index 00000000000..7904fab0726 --- /dev/null +++ b/lib/private/DB/Connection.php @@ -0,0 +1,373 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Driver; +use Doctrine\DBAL\Configuration; +use Doctrine\DBAL\Cache\QueryCacheProfile; +use Doctrine\Common\EventManager; +use OC\DB\QueryBuilder\QueryBuilder; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\PreconditionNotMetException; + +class Connection extends \Doctrine\DBAL\Connection implements IDBConnection { + /** + * @var string $tablePrefix + */ + protected $tablePrefix; + + /** + * @var \OC\DB\Adapter $adapter + */ + protected $adapter; + + public function connect() { + try { + return parent::connect(); + } catch (DBALException $e) { + // throw a new exception to prevent leaking info from the stacktrace + throw new DBALException('Failed to connect to the database: ' . $e->getMessage(), $e->getCode()); + } + } + + /** + * Returns a QueryBuilder for the connection. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder + */ + public function getQueryBuilder() { + return new QueryBuilder($this); + } + + /** + * Gets the QueryBuilder for the connection. + * + * @return \Doctrine\DBAL\Query\QueryBuilder + * @deprecated please use $this->getQueryBuilder() instead + */ + public function createQueryBuilder() { + $backtrace = $this->getCallerBacktrace(); + \OC::$server->getLogger()->debug('Doctrine QueryBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]); + return parent::createQueryBuilder(); + } + + /** + * Gets the ExpressionBuilder for the connection. + * + * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder + * @deprecated please use $this->getQueryBuilder()->expr() instead + */ + public function getExpressionBuilder() { + $backtrace = $this->getCallerBacktrace(); + \OC::$server->getLogger()->debug('Doctrine ExpressionBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]); + return parent::getExpressionBuilder(); + } + + /** + * Get the file and line that called the method where `getCallerBacktrace()` was used + * + * @return string + */ + protected function getCallerBacktrace() { + $traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); + + // 0 is the method where we use `getCallerBacktrace` + // 1 is the target method which uses the method we want to log + if (isset($traces[1])) { + return $traces[1]['file'] . ':' . $traces[1]['line']; + } + + return ''; + } + + /** + * @return string + */ + public function getPrefix() { + return $this->tablePrefix; + } + + /** + * Initializes a new instance of the Connection class. + * + * @param array $params The connection parameters. + * @param \Doctrine\DBAL\Driver $driver + * @param \Doctrine\DBAL\Configuration $config + * @param \Doctrine\Common\EventManager $eventManager + * @throws \Exception + */ + public function __construct(array $params, Driver $driver, Configuration $config = null, + EventManager $eventManager = null) + { + if (!isset($params['adapter'])) { + throw new \Exception('adapter not set'); + } + if (!isset($params['tablePrefix'])) { + throw new \Exception('tablePrefix not set'); + } + parent::__construct($params, $driver, $config, $eventManager); + $this->adapter = new $params['adapter']($this); + $this->tablePrefix = $params['tablePrefix']; + + parent::setTransactionIsolation(parent::TRANSACTION_READ_COMMITTED); + } + + /** + * Prepares an SQL statement. + * + * @param string $statement The SQL statement to prepare. + * @param int $limit + * @param int $offset + * @return \Doctrine\DBAL\Driver\Statement The prepared statement. + */ + public function prepare( $statement, $limit=null, $offset=null ) { + if ($limit === -1) { + $limit = null; + } + if (!is_null($limit)) { + $platform = $this->getDatabasePlatform(); + $statement = $platform->modifyLimitQuery($statement, $limit, $offset); + } + $statement = $this->replaceTablePrefix($statement); + $statement = $this->adapter->fixupStatement($statement); + + if(\OC::$server->getSystemConfig()->getValue( 'log_query', false)) { + \OCP\Util::writeLog('core', 'DB prepare : '.$statement, \OCP\Util::DEBUG); + } + return parent::prepare($statement); + } + + /** + * Executes an, optionally parametrized, SQL query. + * + * If the query is parametrized, a prepared statement is used. + * If an SQLLogger is configured, the execution is logged. + * + * @param string $query The SQL query to execute. + * @param array $params The parameters to bind to the query, if any. + * @param array $types The types the previous parameters are in. + * @param \Doctrine\DBAL\Cache\QueryCacheProfile|null $qcp The query cache profile, optional. + * + * @return \Doctrine\DBAL\Driver\Statement The executed statement. + * + * @throws \Doctrine\DBAL\DBALException + */ + public function executeQuery($query, array $params = array(), $types = array(), QueryCacheProfile $qcp = null) + { + $query = $this->replaceTablePrefix($query); + $query = $this->adapter->fixupStatement($query); + return parent::executeQuery($query, $params, $types, $qcp); + } + + /** + * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters + * and returns the number of affected rows. + * + * This method supports PDO binding types as well as DBAL mapping types. + * + * @param string $query The SQL query. + * @param array $params The query parameters. + * @param array $types The parameter types. + * + * @return integer The number of affected rows. + * + * @throws \Doctrine\DBAL\DBALException + */ + public function executeUpdate($query, array $params = array(), array $types = array()) + { + $query = $this->replaceTablePrefix($query); + $query = $this->adapter->fixupStatement($query); + return parent::executeUpdate($query, $params, $types); + } + + /** + * Returns the ID of the last inserted row, or the last value from a sequence object, + * depending on the underlying driver. + * + * Note: This method may not return a meaningful or consistent result across different drivers, + * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY + * columns or sequences. + * + * @param string $seqName Name of the sequence object from which the ID should be returned. + * @return string A string representation of the last inserted ID. + */ + public function lastInsertId($seqName = null) { + if ($seqName) { + $seqName = $this->replaceTablePrefix($seqName); + } + return $this->adapter->lastInsertId($seqName); + } + + // internal use + public function realLastInsertId($seqName = null) { + return parent::lastInsertId($seqName); + } + + /** + * Insert a row if the matching row does not exists. + * + * @param string $table The table name (will replace *PREFIX* with the actual prefix) + * @param array $input data that should be inserted into the table (column name => value) + * @param array|null $compare List of values that should be checked for "if not exists" + * If this is null or an empty array, all keys of $input will be compared + * Please note: text fields (clob) must not be used in the compare array + * @return int number of inserted rows + * @throws \Doctrine\DBAL\DBALException + */ + public function insertIfNotExist($table, $input, array $compare = null) { + return $this->adapter->insertIfNotExist($table, $input, $compare); + } + + private function getType($value) { + if (is_bool($value)) { + return IQueryBuilder::PARAM_BOOL; + } else if (is_int($value)) { + return IQueryBuilder::PARAM_INT; + } else { + return IQueryBuilder::PARAM_STR; + } + } + + /** + * Insert or update a row value + * + * @param string $table + * @param array $keys (column name => value) + * @param array $values (column name => value) + * @param array $updatePreconditionValues ensure values match preconditions (column name => value) + * @return int number of new rows + * @throws \Doctrine\DBAL\DBALException + * @throws PreconditionNotMetException + */ + public function setValues($table, array $keys, array $values, array $updatePreconditionValues = []) { + try { + $insertQb = $this->getQueryBuilder(); + $insertQb->insert($table) + ->values( + array_map(function($value) use ($insertQb) { + return $insertQb->createNamedParameter($value, $this->getType($value)); + }, array_merge($keys, $values)) + ); + return $insertQb->execute(); + } catch (\Doctrine\DBAL\Exception\ConstraintViolationException $e) { + // value already exists, try update + $updateQb = $this->getQueryBuilder(); + $updateQb->update($table); + foreach ($values as $name => $value) { + $updateQb->set($name, $updateQb->createNamedParameter($value, $this->getType($value))); + } + $where = $updateQb->expr()->andx(); + $whereValues = array_merge($keys, $updatePreconditionValues); + foreach ($whereValues as $name => $value) { + $where->add($updateQb->expr()->eq( + $name, + $updateQb->createNamedParameter($value, $this->getType($value)), + $this->getType($value) + )); + } + $updateQb->where($where); + $affected = $updateQb->execute(); + + if ($affected === 0 && !empty($updatePreconditionValues)) { + throw new PreconditionNotMetException(); + } + + return 0; + } + } + + /** + * returns the error code and message as a string for logging + * works with DoctrineException + * @return string + */ + public function getError() { + $msg = $this->errorCode() . ': '; + $errorInfo = $this->errorInfo(); + if (is_array($errorInfo)) { + $msg .= 'SQLSTATE = '.$errorInfo[0] . ', '; + $msg .= 'Driver Code = '.$errorInfo[1] . ', '; + $msg .= 'Driver Message = '.$errorInfo[2]; + } + return $msg; + } + + /** + * Drop a table from the database if it exists + * + * @param string $table table name without the prefix + */ + public function dropTable($table) { + $table = $this->tablePrefix . trim($table); + $schema = $this->getSchemaManager(); + if($schema->tablesExist(array($table))) { + $schema->dropTable($table); + } + } + + /** + * Check if a table exists + * + * @param string $table table name without the prefix + * @return bool + */ + public function tableExists($table){ + $table = $this->tablePrefix . trim($table); + $schema = $this->getSchemaManager(); + return $schema->tablesExist(array($table)); + } + + // internal use + /** + * @param string $statement + * @return string + */ + protected function replaceTablePrefix($statement) { + return str_replace( '*PREFIX*', $this->tablePrefix, $statement ); + } + + /** + * Check if a transaction is active + * + * @return bool + * @since 8.2.0 + */ + public function inTransaction() { + return $this->getTransactionNestingLevel() > 0; + } + + /** + * Espace a parameter to be used in a LIKE query + * + * @param string $param + * @return string + */ + public function escapeLikeParameter($param) { + return addcslashes($param, '\\_%'); + } +} diff --git a/lib/private/DB/ConnectionFactory.php b/lib/private/DB/ConnectionFactory.php new file mode 100644 index 00000000000..0856d8d19c0 --- /dev/null +++ b/lib/private/DB/ConnectionFactory.php @@ -0,0 +1,184 @@ +<?php +/** + * @author Andreas Fischer <bantu@owncloud.com> + * @author Jörn Friedrich Dreyer <jfd@butonic.de> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; +use Doctrine\DBAL\Event\Listeners\OracleSessionInit; +use Doctrine\DBAL\Event\Listeners\SQLSessionInit; +use Doctrine\DBAL\Event\Listeners\MysqlSessionInit; + +/** +* Takes care of creating and configuring Doctrine connections. +*/ +class ConnectionFactory { + /** + * @var array + * + * Array mapping DBMS type to default connection parameters passed to + * \Doctrine\DBAL\DriverManager::getConnection(). + */ + protected $defaultConnectionParams = array( + 'mysql' => array( + 'adapter' => '\OC\DB\AdapterMySQL', + 'charset' => 'UTF8', + 'driver' => 'pdo_mysql', + 'wrapperClass' => 'OC\DB\Connection', + ), + 'oci' => array( + 'adapter' => '\OC\DB\AdapterOCI8', + 'charset' => 'AL32UTF8', + 'driver' => 'oci8', + 'wrapperClass' => 'OC\DB\OracleConnection', + ), + 'pgsql' => array( + 'adapter' => '\OC\DB\AdapterPgSql', + 'driver' => 'pdo_pgsql', + 'wrapperClass' => 'OC\DB\Connection', + ), + 'sqlite3' => array( + 'adapter' => '\OC\DB\AdapterSqlite', + 'driver' => 'pdo_sqlite', + 'wrapperClass' => 'OC\DB\Connection', + ), + ); + + /** + * @brief Get default connection parameters for a given DBMS. + * @param string $type DBMS type + * @throws \InvalidArgumentException If $type is invalid + * @return array Default connection parameters. + */ + public function getDefaultConnectionParams($type) { + $normalizedType = $this->normalizeType($type); + if (!isset($this->defaultConnectionParams[$normalizedType])) { + throw new \InvalidArgumentException("Unsupported type: $type"); + } + $result = $this->defaultConnectionParams[$normalizedType]; + // \PDO::MYSQL_ATTR_FOUND_ROWS may not be defined, e.g. when the MySQL + // driver is missing. In this case, we won't be able to connect anyway. + if ($normalizedType === 'mysql' && defined('\PDO::MYSQL_ATTR_FOUND_ROWS')) { + $result['driverOptions'] = array( + \PDO::MYSQL_ATTR_FOUND_ROWS => true, + ); + } + return $result; + } + + /** + * @brief Get default connection parameters for a given DBMS. + * @param string $type DBMS type + * @param array $additionalConnectionParams Additional connection parameters + * @return \OC\DB\Connection + */ + public function getConnection($type, $additionalConnectionParams) { + $normalizedType = $this->normalizeType($type); + $eventManager = new \Doctrine\Common\EventManager(); + switch ($normalizedType) { + case 'mysql': + // Send "SET NAMES utf8". Only required on PHP 5.3 below 5.3.6. + // See http://stackoverflow.com/questions/4361459/php-pdo-charset-set-names#4361485 + $eventManager->addEventSubscriber(new MysqlSessionInit); + $eventManager->addEventSubscriber( + new SQLSessionInit("SET SESSION AUTOCOMMIT=1")); + break; + case 'oci': + $eventManager->addEventSubscriber(new OracleSessionInit); + break; + case 'sqlite3': + $journalMode = $additionalConnectionParams['sqlite.journal_mode']; + $additionalConnectionParams['platform'] = new OCSqlitePlatform(); + $eventManager->addEventSubscriber(new SQLiteSessionInit(true, $journalMode)); + break; + } + $connection = \Doctrine\DBAL\DriverManager::getConnection( + array_merge($this->getDefaultConnectionParams($type), $additionalConnectionParams), + new \Doctrine\DBAL\Configuration(), + $eventManager + ); + return $connection; + } + + /** + * @brief Normalize DBMS type + * @param string $type DBMS type + * @return string Normalized DBMS type + */ + public function normalizeType($type) { + return $type === 'sqlite' ? 'sqlite3' : $type; + } + + /** + * @brief Checks whether the specified DBMS type is valid. + * @return bool + */ + public function isValidType($type) { + $normalizedType = $this->normalizeType($type); + return isset($this->defaultConnectionParams[$normalizedType]); + } + + /** + * Create the connection parameters for the config + * + * @param \OC\SystemConfig $config + * @return array + */ + public function createConnectionParams($config) { + $type = $config->getValue('dbtype', 'sqlite'); + + $connectionParams = array( + 'user' => $config->getValue('dbuser', ''), + 'password' => $config->getValue('dbpassword', ''), + ); + $name = $config->getValue('dbname', 'owncloud'); + + if ($this->normalizeType($type) === 'sqlite3') { + $dataDir = $config->getValue("datadirectory", \OC::$SERVERROOT . '/data'); + $connectionParams['path'] = $dataDir . '/' . $name . '.db'; + } else { + $host = $config->getValue('dbhost', ''); + if (strpos($host, ':')) { + // Host variable may carry a port or socket. + list($host, $portOrSocket) = explode(':', $host, 2); + if (ctype_digit($portOrSocket)) { + $connectionParams['port'] = $portOrSocket; + } else { + $connectionParams['unix_socket'] = $portOrSocket; + } + } + $connectionParams['host'] = $host; + $connectionParams['dbname'] = $name; + } + + $connectionParams['tablePrefix'] = $config->getValue('dbtableprefix', 'oc_'); + $connectionParams['sqlite.journal_mode'] = $config->getValue('sqlite.journal_mode', 'WAL'); + + //additional driver options, eg. for mysql ssl + $driverOptions = $config->getValue('dbdriveroptions', null); + if ($driverOptions) { + $connectionParams['driverOptions'] = $driverOptions; + } + + return $connectionParams; + } +} diff --git a/lib/private/DB/MDB2SchemaManager.php b/lib/private/DB/MDB2SchemaManager.php new file mode 100644 index 00000000000..f73f6b4351a --- /dev/null +++ b/lib/private/DB/MDB2SchemaManager.php @@ -0,0 +1,178 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Jörn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Roeland Jago Douma <rullzer@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\OraclePlatform; +use Doctrine\DBAL\Platforms\PostgreSqlPlatform; +use Doctrine\DBAL\Platforms\SqlitePlatform; +use OCP\IDBConnection; + +class MDB2SchemaManager { + /** @var \OC\DB\Connection $conn */ + protected $conn; + + /** + * @param IDBConnection $conn + */ + public function __construct($conn) { + $this->conn = $conn; + } + + /** + * saves database scheme to xml file + * @param string $file name of file + * @return bool + * + * TODO: write more documentation + */ + public function getDbStructure($file) { + return \OC\DB\MDB2SchemaWriter::saveSchemaToFile($file, $this->conn); + } + + /** + * Creates tables from XML file + * @param string $file file to read structure from + * @return bool + * + * TODO: write more documentation + */ + public function createDbFromStructure($file) { + $schemaReader = new MDB2SchemaReader(\OC::$server->getConfig(), $this->conn->getDatabasePlatform()); + $toSchema = $schemaReader->loadSchemaFromFile($file); + return $this->executeSchemaChange($toSchema); + } + + /** + * @return \OC\DB\Migrator + */ + public function getMigrator() { + $random = \OC::$server->getSecureRandom(); + $platform = $this->conn->getDatabasePlatform(); + $config = \OC::$server->getConfig(); + $dispatcher = \OC::$server->getEventDispatcher(); + if ($platform instanceof SqlitePlatform) { + return new SQLiteMigrator($this->conn, $random, $config, $dispatcher); + } else if ($platform instanceof OraclePlatform) { + return new OracleMigrator($this->conn, $random, $config, $dispatcher); + } else if ($platform instanceof MySqlPlatform) { + return new MySQLMigrator($this->conn, $random, $config, $dispatcher); + } else if ($platform instanceof PostgreSqlPlatform) { + return new Migrator($this->conn, $random, $config, $dispatcher); + } else { + return new NoCheckMigrator($this->conn, $random, $config, $dispatcher); + } + } + + /** + * Reads database schema from file + * + * @param string $file file to read from + * @return \Doctrine\DBAL\Schema\Schema + */ + private function readSchemaFromFile($file) { + $platform = $this->conn->getDatabasePlatform(); + $schemaReader = new MDB2SchemaReader(\OC::$server->getConfig(), $platform); + return $schemaReader->loadSchemaFromFile($file); + } + + /** + * update the database scheme + * @param string $file file to read structure from + * @param bool $generateSql only return the sql needed for the upgrade + * @return string|boolean + */ + public function updateDbFromStructure($file, $generateSql = false) { + $toSchema = $this->readSchemaFromFile($file); + $migrator = $this->getMigrator(); + + if ($generateSql) { + return $migrator->generateChangeScript($toSchema); + } else { + $migrator->migrate($toSchema); + return true; + } + } + + /** + * update the database scheme + * @param string $file file to read structure from + * @return boolean + */ + public function simulateUpdateDbFromStructure($file) { + $toSchema = $this->readSchemaFromFile($file); + $this->getMigrator()->checkMigrate($toSchema); + return true; + } + + /** + * @param \Doctrine\DBAL\Schema\Schema $schema + * @return string + */ + public function generateChangeScript($schema) { + $migrator = $this->getMigrator(); + return $migrator->generateChangeScript($schema); + } + + /** + * remove all tables defined in a database structure xml file + * + * @param string $file the xml file describing the tables + */ + public function removeDBStructure($file) { + $schemaReader = new MDB2SchemaReader(\OC::$server->getConfig(), $this->conn->getDatabasePlatform()); + $fromSchema = $schemaReader->loadSchemaFromFile($file); + $toSchema = clone $fromSchema; + /** @var $table \Doctrine\DBAL\Schema\Table */ + foreach ($toSchema->getTables() as $table) { + $toSchema->dropTable($table->getName()); + } + $comparator = new \Doctrine\DBAL\Schema\Comparator(); + $schemaDiff = $comparator->compare($fromSchema, $toSchema); + $this->executeSchemaChange($schemaDiff); + } + + /** + * @param \Doctrine\DBAL\Schema\Schema|\Doctrine\DBAL\Schema\SchemaDiff $schema + * @return bool + */ + private function executeSchemaChange($schema) { + $this->conn->beginTransaction(); + foreach ($schema->toSql($this->conn->getDatabasePlatform()) as $sql) { + $this->conn->query($sql); + } + $this->conn->commit(); + + if ($this->conn->getDatabasePlatform() instanceof SqlitePlatform) { + $this->conn->close(); + $this->conn->connect(); + } + return true; + } +} diff --git a/lib/private/DB/MDB2SchemaReader.php b/lib/private/DB/MDB2SchemaReader.php new file mode 100644 index 00000000000..375fec185cb --- /dev/null +++ b/lib/private/DB/MDB2SchemaReader.php @@ -0,0 +1,359 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Jörn Friedrich Dreyer <jfd@butonic.de> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Oliver Gasser <oliver.gasser@gmail.com> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Victor Dubiniuk <dubiniuk@owncloud.com> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Schema\SchemaConfig; +use OCP\IConfig; + +class MDB2SchemaReader { + /** + * @var string $DBNAME + */ + protected $DBNAME; + + /** + * @var string $DBTABLEPREFIX + */ + protected $DBTABLEPREFIX; + + /** + * @var \Doctrine\DBAL\Platforms\AbstractPlatform $platform + */ + protected $platform; + + /** @var \Doctrine\DBAL\Schema\SchemaConfig $schemaConfig */ + protected $schemaConfig; + + /** + * @param \OCP\IConfig $config + * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + */ + public function __construct(IConfig $config, AbstractPlatform $platform) { + $this->platform = $platform; + $this->DBNAME = $config->getSystemValue('dbname', 'owncloud'); + $this->DBTABLEPREFIX = $config->getSystemValue('dbtableprefix', 'oc_'); + + // Oracle does not support longer index names then 30 characters. + // We use this limit for all DBs to make sure it does not cause a + // problem. + $this->schemaConfig = new SchemaConfig(); + $this->schemaConfig->setMaxIdentifierLength(30); + } + + /** + * @param string $file + * @return \Doctrine\DBAL\Schema\Schema + * @throws \DomainException + */ + public function loadSchemaFromFile($file) { + $schema = new \Doctrine\DBAL\Schema\Schema(); + $loadEntities = libxml_disable_entity_loader(false); + $xml = simplexml_load_file($file); + libxml_disable_entity_loader($loadEntities); + foreach ($xml->children() as $child) { + /** + * @var \SimpleXMLElement $child + */ + switch ($child->getName()) { + case 'name': + case 'create': + case 'overwrite': + case 'charset': + break; + case 'table': + $this->loadTable($schema, $child); + break; + default: + throw new \DomainException('Unknown element: ' . $child->getName()); + + } + } + return $schema; + } + + /** + * @param \Doctrine\DBAL\Schema\Schema $schema + * @param \SimpleXMLElement $xml + * @throws \DomainException + */ + private function loadTable($schema, $xml) { + $table = null; + foreach ($xml->children() as $child) { + /** + * @var \SimpleXMLElement $child + */ + switch ($child->getName()) { + case 'name': + $name = (string)$child; + $name = str_replace('*dbprefix*', $this->DBTABLEPREFIX, $name); + $name = $this->platform->quoteIdentifier($name); + $table = $schema->createTable($name); + $table->addOption('collate', 'utf8_bin'); + $table->setSchemaConfig($this->schemaConfig); + break; + case 'create': + case 'overwrite': + case 'charset': + break; + case 'declaration': + if (is_null($table)) { + throw new \DomainException('Table declaration before table name'); + } + $this->loadDeclaration($table, $child); + break; + default: + throw new \DomainException('Unknown element: ' . $child->getName()); + + } + } + } + + /** + * @param \Doctrine\DBAL\Schema\Table $table + * @param \SimpleXMLElement $xml + * @throws \DomainException + */ + private function loadDeclaration($table, $xml) { + foreach ($xml->children() as $child) { + /** + * @var \SimpleXMLElement $child + */ + switch ($child->getName()) { + case 'field': + $this->loadField($table, $child); + break; + case 'index': + $this->loadIndex($table, $child); + break; + default: + throw new \DomainException('Unknown element: ' . $child->getName()); + + } + } + } + + /** + * @param \Doctrine\DBAL\Schema\Table $table + * @param \SimpleXMLElement $xml + * @throws \DomainException + */ + private function loadField($table, $xml) { + $options = array( 'notnull' => false ); + foreach ($xml->children() as $child) { + /** + * @var \SimpleXMLElement $child + */ + switch ($child->getName()) { + case 'name': + $name = (string)$child; + $name = $this->platform->quoteIdentifier($name); + break; + case 'type': + $type = (string)$child; + switch ($type) { + case 'text': + $type = 'string'; + break; + case 'clob': + $type = 'text'; + break; + case 'timestamp': + $type = 'datetime'; + break; + case 'numeric': + $type = 'decimal'; + break; + } + break; + case 'length': + $length = (string)$child; + $options['length'] = $length; + break; + case 'unsigned': + $unsigned = $this->asBool($child); + $options['unsigned'] = $unsigned; + break; + case 'notnull': + $notnull = $this->asBool($child); + $options['notnull'] = $notnull; + break; + case 'autoincrement': + $autoincrement = $this->asBool($child); + $options['autoincrement'] = $autoincrement; + break; + case 'default': + $default = (string)$child; + $options['default'] = $default; + break; + case 'comments': + $comment = (string)$child; + $options['comment'] = $comment; + break; + case 'primary': + $primary = $this->asBool($child); + $options['primary'] = $primary; + break; + case 'precision': + $precision = (string)$child; + $options['precision'] = $precision; + break; + case 'scale': + $scale = (string)$child; + $options['scale'] = $scale; + break; + default: + throw new \DomainException('Unknown element: ' . $child->getName()); + + } + } + if (isset($name) && isset($type)) { + if (isset($options['default']) && empty($options['default'])) { + if (empty($options['notnull']) || !$options['notnull']) { + unset($options['default']); + $options['notnull'] = false; + } else { + $options['default'] = ''; + } + if ($type == 'integer' || $type == 'decimal') { + $options['default'] = 0; + } elseif ($type == 'boolean') { + $options['default'] = false; + } + if (!empty($options['autoincrement']) && $options['autoincrement']) { + unset($options['default']); + } + } + if ($type === 'integer' && isset($options['default'])) { + $options['default'] = (int)$options['default']; + } + if ($type === 'integer' && isset($options['length'])) { + $length = $options['length']; + if ($length < 4) { + $type = 'smallint'; + } else if ($length > 4) { + $type = 'bigint'; + } + } + if ($type === 'boolean' && isset($options['default'])) { + $options['default'] = $this->asBool($options['default']); + } + if (!empty($options['autoincrement']) + && !empty($options['notnull']) + ) { + $options['primary'] = true; + } + $table->addColumn($name, $type, $options); + if (!empty($options['primary']) && $options['primary']) { + $table->setPrimaryKey(array($name)); + } + } + } + + /** + * @param \Doctrine\DBAL\Schema\Table $table + * @param \SimpleXMLElement $xml + * @throws \DomainException + */ + private function loadIndex($table, $xml) { + $name = null; + $fields = array(); + foreach ($xml->children() as $child) { + /** + * @var \SimpleXMLElement $child + */ + switch ($child->getName()) { + case 'name': + $name = (string)$child; + break; + case 'primary': + $primary = $this->asBool($child); + break; + case 'unique': + $unique = $this->asBool($child); + break; + case 'field': + foreach ($child->children() as $field) { + /** + * @var \SimpleXMLElement $field + */ + switch ($field->getName()) { + case 'name': + $field_name = (string)$field; + $field_name = $this->platform->quoteIdentifier($field_name); + $fields[] = $field_name; + break; + case 'sorting': + break; + default: + throw new \DomainException('Unknown element: ' . $field->getName()); + + } + } + break; + default: + throw new \DomainException('Unknown element: ' . $child->getName()); + + } + } + if (!empty($fields)) { + if (isset($primary) && $primary) { + if ($table->hasPrimaryKey()) { + return; + } + $table->setPrimaryKey($fields, $name); + } else { + if (isset($unique) && $unique) { + $table->addUniqueIndex($fields, $name); + } else { + $table->addIndex($fields, $name); + } + } + } else { + throw new \DomainException('Empty index definition: ' . $name . ' options:' . print_r($fields, true)); + } + } + + /** + * @param \SimpleXMLElement|string $xml + * @return bool + */ + private function asBool($xml) { + $result = (string)$xml; + if ($result == 'true') { + $result = true; + } elseif ($result == 'false') { + $result = false; + } + return (bool)$result; + } + +} diff --git a/lib/private/DB/MDB2SchemaWriter.php b/lib/private/DB/MDB2SchemaWriter.php new file mode 100644 index 00000000000..7dc3bd223a7 --- /dev/null +++ b/lib/private/DB/MDB2SchemaWriter.php @@ -0,0 +1,178 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Jörn Friedrich Dreyer <jfd@butonic.de> + * @author Morris Jobke <hey@morrisjobke.de> + * @author tbelau666 <thomas.belau@gmx.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Schema\Index; + +class MDB2SchemaWriter { + + /** + * @param string $file + * @param \OC\DB\Connection $conn + * @return bool + */ + static public function saveSchemaToFile($file, \OC\DB\Connection $conn) { + $config = \OC::$server->getConfig(); + + $xml = new \SimpleXMLElement('<database/>'); + $xml->addChild('name', $config->getSystemValue('dbname', 'owncloud')); + $xml->addChild('create', 'true'); + $xml->addChild('overwrite', 'false'); + $xml->addChild('charset', 'utf8'); + + // FIX ME: bloody work around + if ($config->getSystemValue('dbtype', 'sqlite') === 'oci') { + $filterExpression = '/^"' . preg_quote($conn->getPrefix()) . '/'; + } else { + $filterExpression = '/^' . preg_quote($conn->getPrefix()) . '/'; + } + $conn->getConfiguration()->setFilterSchemaAssetsExpression($filterExpression); + + foreach ($conn->getSchemaManager()->listTables() as $table) { + self::saveTable($table, $xml->addChild('table')); + } + file_put_contents($file, $xml->asXML()); + return true; + } + + /** + * @param \Doctrine\DBAL\Schema\Table $table + * @param \SimpleXMLElement $xml + */ + private static function saveTable($table, $xml) { + $xml->addChild('name', $table->getName()); + $declaration = $xml->addChild('declaration'); + foreach($table->getColumns() as $column) { + self::saveColumn($column, $declaration->addChild('field')); + } + foreach($table->getIndexes() as $index) { + if ($index->getName() == 'PRIMARY') { + $autoincrement = false; + foreach($index->getColumns() as $column) { + if ($table->getColumn($column)->getAutoincrement()) { + $autoincrement = true; + } + } + if ($autoincrement) { + continue; + } + } + self::saveIndex($index, $declaration->addChild('index')); + } + } + + /** + * @param Column $column + * @param \SimpleXMLElement $xml + */ + private static function saveColumn($column, $xml) { + $xml->addChild('name', $column->getName()); + switch($column->getType()) { + case 'SmallInt': + case 'Integer': + case 'BigInt': + $xml->addChild('type', 'integer'); + $default = $column->getDefault(); + if (is_null($default) && $column->getAutoincrement()) { + $default = '0'; + } + $xml->addChild('default', $default); + $xml->addChild('notnull', self::toBool($column->getNotnull())); + if ($column->getAutoincrement()) { + $xml->addChild('autoincrement', '1'); + } + if ($column->getUnsigned()) { + $xml->addChild('unsigned', 'true'); + } + $length = '4'; + if ($column->getType() == 'SmallInt') { + $length = '2'; + } + elseif ($column->getType() == 'BigInt') { + $length = '8'; + } + $xml->addChild('length', $length); + break; + case 'String': + $xml->addChild('type', 'text'); + $default = trim($column->getDefault()); + if ($default === '') { + $default = false; + } + $xml->addChild('default', $default); + $xml->addChild('notnull', self::toBool($column->getNotnull())); + $xml->addChild('length', $column->getLength()); + break; + case 'Text': + $xml->addChild('type', 'clob'); + $xml->addChild('notnull', self::toBool($column->getNotnull())); + break; + case 'Decimal': + $xml->addChild('type', 'decimal'); + $xml->addChild('default', $column->getDefault()); + $xml->addChild('notnull', self::toBool($column->getNotnull())); + $xml->addChild('length', '15'); + break; + case 'Boolean': + $xml->addChild('type', 'integer'); + $xml->addChild('default', $column->getDefault()); + $xml->addChild('notnull', self::toBool($column->getNotnull())); + $xml->addChild('length', '1'); + break; + case 'DateTime': + $xml->addChild('type', 'timestamp'); + $xml->addChild('default', $column->getDefault()); + $xml->addChild('notnull', self::toBool($column->getNotnull())); + break; + + } + } + + /** + * @param Index $index + * @param \SimpleXMLElement $xml + */ + private static function saveIndex($index, $xml) { + $xml->addChild('name', $index->getName()); + if ($index->isPrimary()) { + $xml->addChild('primary', 'true'); + } + elseif ($index->isUnique()) { + $xml->addChild('unique', 'true'); + } + foreach($index->getColumns() as $column) { + $field = $xml->addChild('field'); + $field->addChild('name', $column); + $field->addChild('sorting', 'ascending'); + + } + } + + private static function toBool($bool) { + return $bool ? 'true' : 'false'; + } +} diff --git a/lib/private/DB/MigrationException.php b/lib/private/DB/MigrationException.php new file mode 100644 index 00000000000..57e4c5b8334 --- /dev/null +++ b/lib/private/DB/MigrationException.php @@ -0,0 +1,40 @@ +<?php +/** + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + + +class MigrationException extends \Exception { + private $table; + + public function __construct($table, $message) { + $this->table = $table; + parent::__construct($message); + } + + /** + * @return string + */ + public function getTable() { + return $this->table; + } +} diff --git a/lib/private/DB/Migrator.php b/lib/private/DB/Migrator.php new file mode 100644 index 00000000000..8b8a34d9865 --- /dev/null +++ b/lib/private/DB/Migrator.php @@ -0,0 +1,292 @@ +<?php +/** + * @author martin-rueegg <martin.rueegg@metaworx.ch> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author tbelau666 <thomas.belau@gmx.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Victor Dubiniuk <dubiniuk@owncloud.com> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + +use \Doctrine\DBAL\DBALException; +use \Doctrine\DBAL\Schema\Index; +use \Doctrine\DBAL\Schema\Table; +use \Doctrine\DBAL\Schema\Schema; +use \Doctrine\DBAL\Schema\SchemaConfig; +use \Doctrine\DBAL\Schema\Comparator; +use OCP\IConfig; +use OCP\Security\ISecureRandom; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\GenericEvent; + +class Migrator { + + /** + * @var \Doctrine\DBAL\Connection $connection + */ + protected $connection; + + /** + * @var ISecureRandom + */ + private $random; + + /** @var IConfig */ + protected $config; + + /** @var EventDispatcher */ + private $dispatcher; + + /** @var bool */ + private $noEmit = false; + + /** + * @param \Doctrine\DBAL\Connection|Connection $connection + * @param ISecureRandom $random + * @param IConfig $config + * @param EventDispatcher $dispatcher + */ + public function __construct(\Doctrine\DBAL\Connection $connection, + ISecureRandom $random, + IConfig $config, + EventDispatcher $dispatcher = null) { + $this->connection = $connection; + $this->random = $random; + $this->config = $config; + $this->dispatcher = $dispatcher; + } + + /** + * @param \Doctrine\DBAL\Schema\Schema $targetSchema + */ + public function migrate(Schema $targetSchema) { + $this->noEmit = true; + $this->applySchema($targetSchema); + } + + /** + * @param \Doctrine\DBAL\Schema\Schema $targetSchema + * @return string + */ + public function generateChangeScript(Schema $targetSchema) { + $schemaDiff = $this->getDiff($targetSchema, $this->connection); + + $script = ''; + $sqls = $schemaDiff->toSql($this->connection->getDatabasePlatform()); + foreach ($sqls as $sql) { + $script .= $this->convertStatementToScript($sql); + } + + return $script; + } + + /** + * @param Schema $targetSchema + * @throws \OC\DB\MigrationException + */ + public function checkMigrate(Schema $targetSchema) { + $this->noEmit = true; + /**@var \Doctrine\DBAL\Schema\Table[] $tables */ + $tables = $targetSchema->getTables(); + $filterExpression = $this->getFilterExpression(); + $this->connection->getConfiguration()-> + setFilterSchemaAssetsExpression($filterExpression); + $existingTables = $this->connection->getSchemaManager()->listTableNames(); + + $step = 0; + foreach ($tables as $table) { + if (strpos($table->getName(), '.')) { + list(, $tableName) = explode('.', $table->getName()); + } else { + $tableName = $table->getName(); + } + $this->emitCheckStep($tableName, $step++, count($tables)); + // don't need to check for new tables + if (array_search($tableName, $existingTables) !== false) { + $this->checkTableMigrate($table); + } + } + } + + /** + * Create a unique name for the temporary table + * + * @param string $name + * @return string + */ + protected function generateTemporaryTableName($name) { + return $this->config->getSystemValue('dbtableprefix', 'oc_') . $name . '_' . $this->random->generate(13, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS); + } + + /** + * Check the migration of a table on a copy so we can detect errors before messing with the real table + * + * @param \Doctrine\DBAL\Schema\Table $table + * @throws \OC\DB\MigrationException + */ + protected function checkTableMigrate(Table $table) { + $name = $table->getName(); + $tmpName = $this->generateTemporaryTableName($name); + + $this->copyTable($name, $tmpName); + + //create the migration schema for the temporary table + $tmpTable = $this->renameTableSchema($table, $tmpName); + $schemaConfig = new SchemaConfig(); + $schemaConfig->setName($this->connection->getDatabase()); + $schema = new Schema(array($tmpTable), array(), $schemaConfig); + + try { + $this->applySchema($schema); + $this->dropTable($tmpName); + } catch (DBALException $e) { + // pgsql needs to commit it's failed transaction before doing anything else + if ($this->connection->isTransactionActive()) { + $this->connection->commit(); + } + $this->dropTable($tmpName); + throw new MigrationException($table->getName(), $e->getMessage()); + } + } + + /** + * @param \Doctrine\DBAL\Schema\Table $table + * @param string $newName + * @return \Doctrine\DBAL\Schema\Table + */ + protected function renameTableSchema(Table $table, $newName) { + /** + * @var \Doctrine\DBAL\Schema\Index[] $indexes + */ + $indexes = $table->getIndexes(); + $newIndexes = array(); + foreach ($indexes as $index) { + if ($index->isPrimary()) { + // do not rename primary key + $indexName = $index->getName(); + } else { + // avoid conflicts in index names + $indexName = $this->config->getSystemValue('dbtableprefix', 'oc_') . $this->random->generate(13, ISecureRandom::CHAR_LOWER); + } + $newIndexes[] = new Index($indexName, $index->getColumns(), $index->isUnique(), $index->isPrimary()); + } + + // foreign keys are not supported so we just set it to an empty array + return new Table($newName, $table->getColumns(), $newIndexes, array(), 0, $table->getOptions()); + } + + protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) { + $filterExpression = $this->getFilterExpression(); + $this->connection->getConfiguration()-> + setFilterSchemaAssetsExpression($filterExpression); + $sourceSchema = $connection->getSchemaManager()->createSchema(); + + // remove tables we don't know about + /** @var $table \Doctrine\DBAL\Schema\Table */ + foreach ($sourceSchema->getTables() as $table) { + if (!$targetSchema->hasTable($table->getName())) { + $sourceSchema->dropTable($table->getName()); + } + } + // remove sequences we don't know about + foreach ($sourceSchema->getSequences() as $table) { + if (!$targetSchema->hasSequence($table->getName())) { + $sourceSchema->dropSequence($table->getName()); + } + } + + $comparator = new Comparator(); + return $comparator->compare($sourceSchema, $targetSchema); + } + + /** + * @param \Doctrine\DBAL\Schema\Schema $targetSchema + * @param \Doctrine\DBAL\Connection $connection + */ + protected function applySchema(Schema $targetSchema, \Doctrine\DBAL\Connection $connection = null) { + if (is_null($connection)) { + $connection = $this->connection; + } + + $schemaDiff = $this->getDiff($targetSchema, $connection); + + $connection->beginTransaction(); + $sqls = $schemaDiff->toSql($connection->getDatabasePlatform()); + $step = 0; + foreach ($sqls as $sql) { + $this->emit($sql, $step++, count($sqls)); + $connection->query($sql); + } + $connection->commit(); + } + + /** + * @param string $sourceName + * @param string $targetName + */ + protected function copyTable($sourceName, $targetName) { + $quotedSource = $this->connection->quoteIdentifier($sourceName); + $quotedTarget = $this->connection->quoteIdentifier($targetName); + + $this->connection->exec('CREATE TABLE ' . $quotedTarget . ' (LIKE ' . $quotedSource . ')'); + $this->connection->exec('INSERT INTO ' . $quotedTarget . ' SELECT * FROM ' . $quotedSource); + } + + /** + * @param string $name + */ + protected function dropTable($name) { + $this->connection->exec('DROP TABLE ' . $this->connection->quoteIdentifier($name)); + } + + /** + * @param $statement + * @return string + */ + protected function convertStatementToScript($statement) { + $script = $statement . ';'; + $script .= PHP_EOL; + $script .= PHP_EOL; + return $script; + } + + protected function getFilterExpression() { + return '/^' . preg_quote($this->config->getSystemValue('dbtableprefix', 'oc_')) . '/'; + } + + protected function emit($sql, $step, $max) { + if ($this->noEmit) { + return; + } + if(is_null($this->dispatcher)) { + return; + } + $this->dispatcher->dispatch('\OC\DB\Migrator::executeSql', new GenericEvent($sql, [$step+1, $max])); + } + + private function emitCheckStep($tableName, $step, $max) { + if(is_null($this->dispatcher)) { + return; + } + $this->dispatcher->dispatch('\OC\DB\Migrator::checkTable', new GenericEvent($tableName, [$step+1, $max])); + } +} diff --git a/lib/private/DB/MySQLMigrator.php b/lib/private/DB/MySQLMigrator.php new file mode 100644 index 00000000000..1b3f70a817d --- /dev/null +++ b/lib/private/DB/MySQLMigrator.php @@ -0,0 +1,51 @@ +<?php +/** + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\Schema\Schema; + +class MySQLMigrator extends Migrator { + /** + * @param Schema $targetSchema + * @param \Doctrine\DBAL\Connection $connection + * @return \Doctrine\DBAL\Schema\SchemaDiff + */ + protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) { + $platform = $connection->getDatabasePlatform(); + $platform->registerDoctrineTypeMapping('enum', 'string'); + $platform->registerDoctrineTypeMapping('bit', 'string'); + + $schemaDiff = parent::getDiff($targetSchema, $connection); + + // identifiers need to be quoted for mysql + foreach ($schemaDiff->changedTables as $tableDiff) { + $tableDiff->name = $this->connection->quoteIdentifier($tableDiff->name); + foreach ($tableDiff->changedColumns as $column) { + $column->oldColumnName = $this->connection->quoteIdentifier($column->oldColumnName); + } + } + + return $schemaDiff; + } +} diff --git a/lib/private/DB/NoCheckMigrator.php b/lib/private/DB/NoCheckMigrator.php new file mode 100644 index 00000000000..23afae11816 --- /dev/null +++ b/lib/private/DB/NoCheckMigrator.php @@ -0,0 +1,38 @@ +<?php +/** + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\Schema\Schema; + +/** + * migrator for database platforms that don't support the upgrade check + * + * @package OC\DB + */ +class NoCheckMigrator extends Migrator { + /** + * @param \Doctrine\DBAL\Schema\Schema $targetSchema + * @throws \OC\DB\MigrationException + */ + public function checkMigrate(Schema $targetSchema) {} +} diff --git a/lib/private/DB/OCSqlitePlatform.php b/lib/private/DB/OCSqlitePlatform.php new file mode 100644 index 00000000000..fe68bc3cc91 --- /dev/null +++ b/lib/private/DB/OCSqlitePlatform.php @@ -0,0 +1,48 @@ +<?php +/** + * @author Robin Appelman <icewind@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + +class OCSqlitePlatform extends \Doctrine\DBAL\Platforms\SqlitePlatform { + /** + * {@inheritDoc} + */ + public function getColumnDeclarationSQL($name, array $field) { + $def = parent::getColumnDeclarationSQL($name, $field); + if (!empty($field['autoincrement'])) { + $def .= ' PRIMARY KEY AUTOINCREMENT'; + } + return $def; + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($name, array $columns, array $options = array()){ + // if auto increment is set the column is already defined as primary key + foreach ($columns as $column) { + if (!empty($column['autoincrement'])) { + $options['primary'] = null; + } + } + return parent::_getCreateTableSQL($name, $columns, $options); + } +} diff --git a/lib/private/DB/OracleConnection.php b/lib/private/DB/OracleConnection.php new file mode 100644 index 00000000000..a95f37a8e6c --- /dev/null +++ b/lib/private/DB/OracleConnection.php @@ -0,0 +1,93 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + +class OracleConnection extends Connection { + /** + * Quote the keys of the array + */ + private function quoteKeys(array $data) { + $return = array(); + foreach($data as $key => $value) { + $return[$this->quoteIdentifier($key)] = $value; + } + return $return; + } + + /** + * {@inheritDoc} + */ + public function insert($tableName, array $data, array $types = array()) { + $tableName = $this->quoteIdentifier($tableName); + $data = $this->quoteKeys($data); + return parent::insert($tableName, $data, $types); + } + + /** + * {@inheritDoc} + */ + public function update($tableName, array $data, array $identifier, array $types = array()) { + $tableName = $this->quoteIdentifier($tableName); + $data = $this->quoteKeys($data); + $identifier = $this->quoteKeys($identifier); + return parent::update($tableName, $data, $identifier, $types); + } + + /** + * {@inheritDoc} + */ + public function delete($tableExpression, array $identifier, array $types = array()) { + $tableName = $this->quoteIdentifier($tableExpression); + $identifier = $this->quoteKeys($identifier); + return parent::delete($tableName, $identifier); + } + + /** + * Drop a table from the database if it exists + * + * @param string $table table name without the prefix + */ + public function dropTable($table) { + $table = $this->tablePrefix . trim($table); + $table = $this->quoteIdentifier($table); + $schema = $this->getSchemaManager(); + if($schema->tablesExist(array($table))) { + $schema->dropTable($table); + } + } + + /** + * Check if a table exists + * + * @param string $table table name without the prefix + * @return bool + */ + public function tableExists($table){ + $table = $this->tablePrefix . trim($table); + $table = $this->quoteIdentifier($table); + $schema = $this->getSchemaManager(); + return $schema->tablesExist(array($table)); + } +} diff --git a/lib/private/DB/OracleMigrator.php b/lib/private/DB/OracleMigrator.php new file mode 100644 index 00000000000..ceb89cf64d4 --- /dev/null +++ b/lib/private/DB/OracleMigrator.php @@ -0,0 +1,74 @@ +<?php +/** + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\Schema\Schema; + +class OracleMigrator extends NoCheckMigrator { + /** + * @param Schema $targetSchema + * @param \Doctrine\DBAL\Connection $connection + * @return \Doctrine\DBAL\Schema\SchemaDiff + */ + protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) { + $schemaDiff = parent::getDiff($targetSchema, $connection); + + // oracle forces us to quote the identifiers + foreach ($schemaDiff->changedTables as $tableDiff) { + $tableDiff->name = $this->connection->quoteIdentifier($tableDiff->name); + foreach ($tableDiff->changedColumns as $column) { + $column->oldColumnName = $this->connection->quoteIdentifier($column->oldColumnName); + } + } + + return $schemaDiff; + } + + /** + * @param string $name + * @return string + */ + protected function generateTemporaryTableName($name) { + return 'oc_' . uniqid(); + } + + /** + * @param $statement + * @return string + */ + protected function convertStatementToScript($statement) { + if (substr($statement, -1) === ';') { + return $statement . PHP_EOL . '/' . PHP_EOL; + } + $script = $statement . ';'; + $script .= PHP_EOL; + $script .= PHP_EOL; + return $script; + } + + protected function getFilterExpression() { + return '/^"' . preg_quote($this->config->getSystemValue('dbtableprefix', 'oc_')) . '/'; + } + +} diff --git a/lib/private/DB/PgSqlTools.php b/lib/private/DB/PgSqlTools.php new file mode 100644 index 00000000000..e9e507551e6 --- /dev/null +++ b/lib/private/DB/PgSqlTools.php @@ -0,0 +1,70 @@ +<?php +/** + * @author Andreas Fischer <bantu@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author tbelau666 <thomas.belau@gmx.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; +use OCP\IConfig; + +/** +* Various PostgreSQL specific helper functions. +*/ +class PgSqlTools { + + /** @var \OCP\IConfig */ + private $config; + + /** + * @param \OCP\IConfig $config + */ + public function __construct(IConfig $config) { + $this->config = $config; + } + + /** + * @brief Resynchronizes all sequences of a database after using INSERTs + * without leaving out the auto-incremented column. + * @param \OC\DB\Connection $conn + * @return null + */ + public function resynchronizeDatabaseSequences(Connection $conn) { + $filterExpression = '/^' . preg_quote($this->config->getSystemValue('dbtableprefix', 'oc_')) . '/'; + $databaseName = $conn->getDatabase(); + $conn->getConfiguration()->setFilterSchemaAssetsExpression($filterExpression); + + foreach ($conn->getSchemaManager()->listSequences() as $sequence) { + $sequenceName = $sequence->getName(); + $sqlInfo = 'SELECT table_schema, table_name, column_name + FROM information_schema.columns + WHERE column_default = ? AND table_catalog = ?'; + $sequenceInfo = $conn->fetchAssoc($sqlInfo, array( + "nextval('$sequenceName'::regclass)", + $databaseName + )); + $tableName = $sequenceInfo['table_name']; + $columnName = $sequenceInfo['column_name']; + $sqlMaxId = "SELECT MAX($columnName) FROM $tableName"; + $sqlSetval = "SELECT setval('$sequenceName', ($sqlMaxId))"; + $conn->executeQuery($sqlSetval); + } + } +} diff --git a/lib/private/DB/QueryBuilder/CompositeExpression.php b/lib/private/DB/QueryBuilder/CompositeExpression.php new file mode 100644 index 00000000000..927dfe38378 --- /dev/null +++ b/lib/private/DB/QueryBuilder/CompositeExpression.php @@ -0,0 +1,92 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB\QueryBuilder; + +use OCP\DB\QueryBuilder\ICompositeExpression; + +class CompositeExpression implements ICompositeExpression, \Countable { + /** @var \Doctrine\DBAL\Query\Expression\CompositeExpression */ + protected $compositeExpression; + + /** + * Constructor. + * + * @param \Doctrine\DBAL\Query\Expression\CompositeExpression $compositeExpression + */ + public function __construct(\Doctrine\DBAL\Query\Expression\CompositeExpression $compositeExpression) { + $this->compositeExpression = $compositeExpression; + } + + /** + * Adds multiple parts to composite expression. + * + * @param array $parts + * + * @return \OCP\DB\QueryBuilder\ICompositeExpression + */ + public function addMultiple(array $parts = array()) { + $this->compositeExpression->addMultiple($parts); + + return $this; + } + + /** + * Adds an expression to composite expression. + * + * @param mixed $part + * + * @return \OCP\DB\QueryBuilder\ICompositeExpression + */ + public function add($part) { + $this->compositeExpression->add($part); + + return $this; + } + + /** + * Retrieves the amount of expressions on composite expression. + * + * @return integer + */ + public function count() { + return $this->compositeExpression->count(); + } + + /** + * Returns the type of this composite expression (AND/OR). + * + * @return string + */ + public function getType() { + return $this->compositeExpression->getType(); + } + + /** + * Retrieves the string representation of this composite expression. + * + * @return string + */ + public function __toString() + { + return (string) $this->compositeExpression; + } +} diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php new file mode 100644 index 00000000000..ce98816c42d --- /dev/null +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/ExpressionBuilder.php @@ -0,0 +1,370 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB\QueryBuilder\ExpressionBuilder; + +use Doctrine\DBAL\Query\Expression\ExpressionBuilder as DoctrineExpressionBuilder; +use OC\DB\QueryBuilder\CompositeExpression; +use OC\DB\QueryBuilder\Literal; +use OC\DB\QueryBuilder\QueryFunction; +use OC\DB\QueryBuilder\QuoteHelper; +use OCP\DB\QueryBuilder\IExpressionBuilder; +use OCP\IDBConnection; + +class ExpressionBuilder implements IExpressionBuilder { + /** @var \Doctrine\DBAL\Query\Expression\ExpressionBuilder */ + protected $expressionBuilder; + + /** @var QuoteHelper */ + protected $helper; + + /** + * Initializes a new <tt>ExpressionBuilder</tt>. + * + * @param \OCP\IDBConnection $connection + */ + public function __construct(IDBConnection $connection) { + $this->helper = new QuoteHelper(); + $this->expressionBuilder = new DoctrineExpressionBuilder($connection); + } + + /** + * Creates a conjunction of the given boolean expressions. + * + * Example: + * + * [php] + * // (u.type = ?) AND (u.role = ?) + * $expr->andX('u.type = ?', 'u.role = ?')); + * + * @param mixed $x Optional clause. Defaults = null, but requires + * at least one defined when converting to string. + * + * @return \OCP\DB\QueryBuilder\ICompositeExpression + */ + public function andX($x = null) { + $arguments = func_get_args(); + $compositeExpression = call_user_func_array([$this->expressionBuilder, 'andX'], $arguments); + return new CompositeExpression($compositeExpression); + } + + /** + * Creates a disjunction of the given boolean expressions. + * + * Example: + * + * [php] + * // (u.type = ?) OR (u.role = ?) + * $qb->where($qb->expr()->orX('u.type = ?', 'u.role = ?')); + * + * @param mixed $x Optional clause. Defaults = null, but requires + * at least one defined when converting to string. + * + * @return \OCP\DB\QueryBuilder\ICompositeExpression + */ + public function orX($x = null) { + $arguments = func_get_args(); + $compositeExpression = call_user_func_array([$this->expressionBuilder, 'orX'], $arguments); + return new CompositeExpression($compositeExpression); + } + + /** + * Creates a comparison expression. + * + * @param mixed $x The left expression. + * @param string $operator One of the IExpressionBuilder::* constants. + * @param mixed $y The right expression. + * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants + * required when comparing text fields for oci compatibility + * + * @return string + */ + public function comparison($x, $operator, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->comparison($x, $operator, $y); + } + + /** + * Creates an equality comparison expression with the given arguments. + * + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <left expr> = <right expr>. Example: + * + * [php] + * // u.id = ? + * $expr->eq('u.id', '?'); + * + * @param mixed $x The left expression. + * @param mixed $y The right expression. + * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants + * required when comparing text fields for oci compatibility + * + * @return string + */ + public function eq($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->eq($x, $y); + } + + /** + * Creates a non equality comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <left expr> <> <right expr>. Example: + * + * [php] + * // u.id <> 1 + * $q->where($q->expr()->neq('u.id', '1')); + * + * @param mixed $x The left expression. + * @param mixed $y The right expression. + * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants + * required when comparing text fields for oci compatibility + * + * @return string + */ + public function neq($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->neq($x, $y); + } + + /** + * Creates a lower-than comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <left expr> < <right expr>. Example: + * + * [php] + * // u.id < ? + * $q->where($q->expr()->lt('u.id', '?')); + * + * @param mixed $x The left expression. + * @param mixed $y The right expression. + * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants + * required when comparing text fields for oci compatibility + * + * @return string + */ + public function lt($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->lt($x, $y); + } + + /** + * Creates a lower-than-equal comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <left expr> <= <right expr>. Example: + * + * [php] + * // u.id <= ? + * $q->where($q->expr()->lte('u.id', '?')); + * + * @param mixed $x The left expression. + * @param mixed $y The right expression. + * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants + * required when comparing text fields for oci compatibility + * + * @return string + */ + public function lte($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->lte($x, $y); + } + + /** + * Creates a greater-than comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <left expr> > <right expr>. Example: + * + * [php] + * // u.id > ? + * $q->where($q->expr()->gt('u.id', '?')); + * + * @param mixed $x The left expression. + * @param mixed $y The right expression. + * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants + * required when comparing text fields for oci compatibility + * + * @return string + */ + public function gt($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->gt($x, $y); + } + + /** + * Creates a greater-than-equal comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <left expr> >= <right expr>. Example: + * + * [php] + * // u.id >= ? + * $q->where($q->expr()->gte('u.id', '?')); + * + * @param mixed $x The left expression. + * @param mixed $y The right expression. + * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants + * required when comparing text fields for oci compatibility + * + * @return string + */ + public function gte($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->gte($x, $y); + } + + /** + * Creates an IS NULL expression with the given arguments. + * + * @param string $x The field in string format to be restricted by IS NULL. + * + * @return string + */ + public function isNull($x) { + $x = $this->helper->quoteColumnName($x); + return $this->expressionBuilder->isNull($x); + } + + /** + * Creates an IS NOT NULL expression with the given arguments. + * + * @param string $x The field in string format to be restricted by IS NOT NULL. + * + * @return string + */ + public function isNotNull($x) { + $x = $this->helper->quoteColumnName($x); + return $this->expressionBuilder->isNotNull($x); + } + + /** + * Creates a LIKE() comparison expression with the given arguments. + * + * @param string $x Field in string format to be inspected by LIKE() comparison. + * @param mixed $y Argument to be used in LIKE() comparison. + * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants + * required when comparing text fields for oci compatibility + * + * @return string + */ + public function like($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->like($x, $y); + } + + /** + * Creates a ILIKE() comparison expression with the given arguments. + * + * @param string $x Field in string format to be inspected by ILIKE() comparison. + * @param mixed $y Argument to be used in ILIKE() comparison. + * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants + * required when comparing text fields for oci compatibility + * + * @return string + * @since 9.0.0 + */ + public function iLike($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->comparison("LOWER($x)", 'LIKE', "LOWER($y)"); + } + + /** + * Creates a NOT LIKE() comparison expression with the given arguments. + * + * @param string $x Field in string format to be inspected by NOT LIKE() comparison. + * @param mixed $y Argument to be used in NOT LIKE() comparison. + * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants + * required when comparing text fields for oci compatibility + * + * @return string + */ + public function notLike($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->notLike($x, $y); + } + + /** + * Creates a IN () comparison expression with the given arguments. + * + * @param string $x The field in string format to be inspected by IN() comparison. + * @param string|array $y The placeholder or the array of values to be used by IN() comparison. + * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants + * required when comparing text fields for oci compatibility + * + * @return string + */ + public function in($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnNames($y); + return $this->expressionBuilder->in($x, $y); + } + + /** + * Creates a NOT IN () comparison expression with the given arguments. + * + * @param string $x The field in string format to be inspected by NOT IN() comparison. + * @param string|array $y The placeholder or the array of values to be used by NOT IN() comparison. + * @param mixed|null $type one of the IQueryBuilder::PARAM_* constants + * required when comparing text fields for oci compatibility + * + * @return string + */ + public function notIn($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnNames($y); + return $this->expressionBuilder->notIn($x, $y); + } + + /** + * Quotes a given input parameter. + * + * @param mixed $input The parameter to be quoted. + * @param mixed|null $type One of the IQueryBuilder::PARAM_* constants + * + * @return Literal + */ + public function literal($input, $type = null) { + return new Literal($this->expressionBuilder->literal($input, $type)); + } + + /** + * Returns a IQueryFunction that casts the column to the given type + * + * @param string $column + * @param mixed $type One of IQueryBuilder::PARAM_* + * @return string + */ + public function castColumn($column, $type) { + return new QueryFunction( + $this->helper->quoteColumnName($column) + ); + } +} diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php new file mode 100644 index 00000000000..0d34787d26a --- /dev/null +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/MySqlExpressionBuilder.php @@ -0,0 +1,40 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB\QueryBuilder\ExpressionBuilder; + + +use OC\DB\QueryBuilder\QueryFunction; +use OCP\DB\QueryBuilder\IQueryBuilder; + +class MySqlExpressionBuilder extends ExpressionBuilder { + + /** + * @inheritdoc + */ + public function iLike($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->comparison($x, ' COLLATE utf8_general_ci LIKE', $y); + } + +} diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/OCIExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/OCIExpressionBuilder.php new file mode 100644 index 00000000000..5d615ae52e8 --- /dev/null +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/OCIExpressionBuilder.php @@ -0,0 +1,163 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB\QueryBuilder\ExpressionBuilder; + + +use OC\DB\QueryBuilder\QueryFunction; +use OCP\DB\QueryBuilder\ILiteral; +use OCP\DB\QueryBuilder\IParameter; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\DB\QueryBuilder\IQueryFunction; + +class OCIExpressionBuilder extends ExpressionBuilder { + + /** + * @param mixed $column + * @param mixed|null $type + * @return array|IQueryFunction|string + */ + protected function prepareColumn($column, $type) { + if ($type === IQueryBuilder::PARAM_STR && !is_array($column) && !($column instanceof IParameter) && !($column instanceof ILiteral)) { + $column = $this->castColumn($column, $type); + } else { + $column = $this->helper->quoteColumnNames($column); + } + return $column; + } + + /** + * @inheritdoc + */ + public function comparison($x, $operator, $y, $type = null) { + $x = $this->prepareColumn($x, $type); + $y = $this->prepareColumn($y, $type); + + return $this->expressionBuilder->comparison($x, $operator, $y); + } + + /** + * @inheritdoc + */ + public function eq($x, $y, $type = null) { + $x = $this->prepareColumn($x, $type); + $y = $this->prepareColumn($y, $type); + + return $this->expressionBuilder->eq($x, $y); + } + + /** + * @inheritdoc + */ + public function neq($x, $y, $type = null) { + $x = $this->prepareColumn($x, $type); + $y = $this->prepareColumn($y, $type); + + return $this->expressionBuilder->neq($x, $y); + } + + /** + * @inheritdoc + */ + public function lt($x, $y, $type = null) { + $x = $this->prepareColumn($x, $type); + $y = $this->prepareColumn($y, $type); + + return $this->expressionBuilder->lt($x, $y); + } + + /** + * @inheritdoc + */ + public function lte($x, $y, $type = null) { + $x = $this->prepareColumn($x, $type); + $y = $this->prepareColumn($y, $type); + + return $this->expressionBuilder->lte($x, $y); + } + + /** + * @inheritdoc + */ + public function gt($x, $y, $type = null) { + $x = $this->prepareColumn($x, $type); + $y = $this->prepareColumn($y, $type); + + return $this->expressionBuilder->gt($x, $y); + } + + /** + * @inheritdoc + */ + public function gte($x, $y, $type = null) { + $x = $this->prepareColumn($x, $type); + $y = $this->prepareColumn($y, $type); + + return $this->expressionBuilder->gte($x, $y); + } + + /** + * @inheritdoc + */ + public function in($x, $y, $type = null) { + $x = $this->prepareColumn($x, $type); + $y = $this->prepareColumn($y, $type); + + return $this->expressionBuilder->in($x, $y); + } + + /** + * @inheritdoc + */ + public function notIn($x, $y, $type = null) { + $x = $this->prepareColumn($x, $type); + $y = $this->prepareColumn($y, $type); + + return $this->expressionBuilder->notIn($x, $y); + } + + /** + * Returns a IQueryFunction that casts the column to the given type + * + * @param string $column + * @param mixed $type One of IQueryBuilder::PARAM_* + * @return IQueryFunction + */ + public function castColumn($column, $type) { + if ($type === IQueryBuilder::PARAM_STR) { + $column = $this->helper->quoteColumnName($column); + return new QueryFunction('to_char(' . $column . ')'); + } + + return parent::castColumn($column, $type); + } + + /** + * @inheritdoc + */ + public function iLike($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return new QueryFunction('REGEXP_LIKE('.$x.', \'^\' || REPLACE('.$y.', \'%\', \'.*\') || \'$\', \'i\')'); + } +} diff --git a/lib/private/DB/QueryBuilder/ExpressionBuilder/PgSqlExpressionBuilder.php b/lib/private/DB/QueryBuilder/ExpressionBuilder/PgSqlExpressionBuilder.php new file mode 100644 index 00000000000..a8f1af87a45 --- /dev/null +++ b/lib/private/DB/QueryBuilder/ExpressionBuilder/PgSqlExpressionBuilder.php @@ -0,0 +1,56 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB\QueryBuilder\ExpressionBuilder; + + +use OC\DB\QueryBuilder\QueryFunction; +use OCP\DB\QueryBuilder\IQueryBuilder; + +class PgSqlExpressionBuilder extends ExpressionBuilder { + + /** + * Returns a IQueryFunction that casts the column to the given type + * + * @param string $column + * @param mixed $type One of IQueryBuilder::PARAM_* + * @return string + */ + public function castColumn($column, $type) { + if ($type === IQueryBuilder::PARAM_INT) { + $column = $this->helper->quoteColumnName($column); + return new QueryFunction('CAST(' . $column . ' AS INT)'); + } + + return parent::castColumn($column, $type); + } + + /** + * @inheritdoc + */ + public function iLike($x, $y, $type = null) { + $x = $this->helper->quoteColumnName($x); + $y = $this->helper->quoteColumnName($y); + return $this->expressionBuilder->comparison($x, 'ILIKE', $y); + } + +} diff --git a/lib/private/DB/QueryBuilder/Literal.php b/lib/private/DB/QueryBuilder/Literal.php new file mode 100644 index 00000000000..0cc96ab48b4 --- /dev/null +++ b/lib/private/DB/QueryBuilder/Literal.php @@ -0,0 +1,40 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB\QueryBuilder; + +use OCP\DB\QueryBuilder\ILiteral; + +class Literal implements ILiteral{ + /** @var mixed */ + protected $literal; + + public function __construct($literal) { + $this->literal = $literal; + } + + /** + * @return string + */ + public function __toString() { + return (string) $this->literal; + } +} diff --git a/lib/private/DB/QueryBuilder/Parameter.php b/lib/private/DB/QueryBuilder/Parameter.php new file mode 100644 index 00000000000..1c233c83f1d --- /dev/null +++ b/lib/private/DB/QueryBuilder/Parameter.php @@ -0,0 +1,40 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB\QueryBuilder; + +use OCP\DB\QueryBuilder\IParameter; + +class Parameter implements IParameter { + /** @var mixed */ + protected $name; + + public function __construct($name) { + $this->name = $name; + } + + /** + * @return string + */ + public function __toString() { + return (string) $this->name; + } +} diff --git a/lib/private/DB/QueryBuilder/QueryBuilder.php b/lib/private/DB/QueryBuilder/QueryBuilder.php new file mode 100644 index 00000000000..bb463e43a75 --- /dev/null +++ b/lib/private/DB/QueryBuilder/QueryBuilder.php @@ -0,0 +1,1124 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB\QueryBuilder; + +use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\PostgreSqlPlatform; +use OC\DB\OracleConnection; +use OC\DB\QueryBuilder\ExpressionBuilder\ExpressionBuilder; +use OC\DB\QueryBuilder\ExpressionBuilder\MySqlExpressionBuilder; +use OC\DB\QueryBuilder\ExpressionBuilder\OCIExpressionBuilder; +use OC\DB\QueryBuilder\ExpressionBuilder\PgSqlExpressionBuilder; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\DB\QueryBuilder\IQueryFunction; +use OCP\DB\QueryBuilder\IParameter; +use OCP\IDBConnection; + +class QueryBuilder implements IQueryBuilder { + + /** @var \OCP\IDBConnection */ + private $connection; + + /** @var \Doctrine\DBAL\Query\QueryBuilder */ + private $queryBuilder; + + /** @var QuoteHelper */ + private $helper; + + /** @var bool */ + private $automaticTablePrefix = true; + + /** @var string */ + protected $lastInsertedTable; + + /** + * Initializes a new QueryBuilder. + * + * @param \OCP\IDBConnection $connection + */ + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + $this->queryBuilder = new \Doctrine\DBAL\Query\QueryBuilder($this->connection); + $this->helper = new QuoteHelper(); + } + + /** + * Enable/disable automatic prefixing of table names with the oc_ prefix + * + * @param bool $enabled If set to true table names will be prefixed with the + * owncloud database prefix automatically. + * @since 8.2.0 + */ + public function automaticTablePrefix($enabled) { + $this->automaticTablePrefix = (bool) $enabled; + } + + /** + * Gets an ExpressionBuilder used for object-oriented construction of query expressions. + * This producer method is intended for convenient inline usage. Example: + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where($qb->expr()->eq('u.id', 1)); + * </code> + * + * For more complex expression construction, consider storing the expression + * builder object in a local variable. + * + * @return \OCP\DB\QueryBuilder\IExpressionBuilder + */ + public function expr() { + if ($this->connection instanceof OracleConnection) { + return new OCIExpressionBuilder($this->connection); + } else if ($this->connection->getDatabasePlatform() instanceof PostgreSqlPlatform) { + return new PgSqlExpressionBuilder($this->connection); + } else if ($this->connection->getDatabasePlatform() instanceof MySqlPlatform) { + return new MySqlExpressionBuilder($this->connection); + } else { + return new ExpressionBuilder($this->connection); + } + } + + /** + * Gets the type of the currently built query. + * + * @return integer + */ + public function getType() { + return $this->queryBuilder->getType(); + } + + /** + * Gets the associated DBAL Connection for this query builder. + * + * @return \OCP\IDBConnection + */ + public function getConnection() { + return $this->connection; + } + + /** + * Gets the state of this query builder instance. + * + * @return integer Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN. + */ + public function getState() { + return $this->queryBuilder->getState(); + } + + /** + * Executes this query using the bound parameters and their types. + * + * Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeUpdate} + * for insert, update and delete statements. + * + * @return \Doctrine\DBAL\Driver\Statement|int + */ + public function execute() { + return $this->queryBuilder->execute(); + } + + /** + * Gets the complete SQL string formed by the current specifications of this QueryBuilder. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * echo $qb->getSQL(); // SELECT u FROM User u + * </code> + * + * @return string The SQL query string. + */ + public function getSQL() { + return $this->queryBuilder->getSQL(); + } + + /** + * Sets a query parameter for the query being constructed. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where('u.id = :user_id') + * ->setParameter(':user_id', 1); + * </code> + * + * @param string|integer $key The parameter position or name. + * @param mixed $value The parameter value. + * @param string|null $type One of the IQueryBuilder::PARAM_* constants. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function setParameter($key, $value, $type = null) { + $this->queryBuilder->setParameter($key, $value, $type); + + return $this; + } + + /** + * Sets a collection of query parameters for the query being constructed. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where('u.id = :user_id1 OR u.id = :user_id2') + * ->setParameters(array( + * ':user_id1' => 1, + * ':user_id2' => 2 + * )); + * </code> + * + * @param array $params The query parameters to set. + * @param array $types The query parameters types to set. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function setParameters(array $params, array $types = array()) { + $this->queryBuilder->setParameters($params, $types); + + return $this; + } + + /** + * Gets all defined query parameters for the query being constructed indexed by parameter index or name. + * + * @return array The currently defined query parameters indexed by parameter index or name. + */ + public function getParameters() { + return $this->queryBuilder->getParameters(); + } + + /** + * Gets a (previously set) query parameter of the query being constructed. + * + * @param mixed $key The key (index or name) of the bound parameter. + * + * @return mixed The value of the bound parameter. + */ + public function getParameter($key) { + return $this->queryBuilder->getParameter($key); + } + + /** + * Gets all defined query parameter types for the query being constructed indexed by parameter index or name. + * + * @return array The currently defined query parameter types indexed by parameter index or name. + */ + public function getParameterTypes() { + return $this->queryBuilder->getParameterTypes(); + } + + /** + * Gets a (previously set) query parameter type of the query being constructed. + * + * @param mixed $key The key (index or name) of the bound parameter type. + * + * @return mixed The value of the bound parameter type. + */ + public function getParameterType($key) { + return $this->queryBuilder->getParameterType($key); + } + + /** + * Sets the position of the first result to retrieve (the "offset"). + * + * @param integer $firstResult The first result to return. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function setFirstResult($firstResult) { + $this->queryBuilder->setFirstResult($firstResult); + + return $this; + } + + /** + * Gets the position of the first result the query object was set to retrieve (the "offset"). + * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder. + * + * @return integer The position of the first result. + */ + public function getFirstResult() { + return $this->queryBuilder->getFirstResult(); + } + + /** + * Sets the maximum number of results to retrieve (the "limit"). + * + * NOTE: Setting max results to "0" will cause mixed behaviour. While most + * of the databases will just return an empty result set, Oracle will return + * all entries. + * + * @param integer $maxResults The maximum number of results to retrieve. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function setMaxResults($maxResults) { + $this->queryBuilder->setMaxResults($maxResults); + + return $this; + } + + /** + * Gets the maximum number of results the query object was set to retrieve (the "limit"). + * Returns NULL if {@link setMaxResults} was not applied to this query builder. + * + * @return integer The maximum number of results. + */ + public function getMaxResults() { + return $this->queryBuilder->getMaxResults(); + } + + /** + * Specifies an item that is to be returned in the query result. + * Replaces any previously specified selections, if any. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u.id', 'p.id') + * ->from('users', 'u') + * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); + * </code> + * + * @param mixed $select The selection expressions. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function select($select = null) { + $selects = is_array($select) ? $select : func_get_args(); + + $this->queryBuilder->select( + $this->helper->quoteColumnNames($selects) + ); + + return $this; + } + + /** + * Specifies an item that is to be returned with a different name in the query result. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->selectAlias('u.id', 'user_id') + * ->from('users', 'u') + * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); + * </code> + * + * @param mixed $select The selection expressions. + * @param string $alias The column alias used in the constructed query. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function selectAlias($select, $alias) { + + $this->queryBuilder->addSelect( + $this->helper->quoteColumnName($select) . ' AS ' . $this->helper->quoteColumnName($alias) + ); + + return $this; + } + + /** + * Specifies an item that is to be returned uniquely in the query result. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->selectDistinct('type') + * ->from('users'); + * </code> + * + * @param mixed $select The selection expressions. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function selectDistinct($select) { + + $this->queryBuilder->addSelect( + 'DISTINCT ' . $this->helper->quoteColumnName($select) + ); + + return $this; + } + + /** + * Adds an item that is to be returned in the query result. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u.id') + * ->addSelect('p.id') + * ->from('users', 'u') + * ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id'); + * </code> + * + * @param mixed $select The selection expression. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function addSelect($select = null) { + $selects = is_array($select) ? $select : func_get_args(); + + $this->queryBuilder->addSelect( + $this->helper->quoteColumnNames($selects) + ); + + return $this; + } + + /** + * Turns the query being built into a bulk delete query that ranges over + * a certain table. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->delete('users', 'u') + * ->where('u.id = :user_id'); + * ->setParameter(':user_id', 1); + * </code> + * + * @param string $delete The table whose rows are subject to the deletion. + * @param string $alias The table alias used in the constructed query. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function delete($delete = null, $alias = null) { + $this->queryBuilder->delete( + $this->getTableName($delete), + $alias + ); + + return $this; + } + + /** + * Turns the query being built into a bulk update query that ranges over + * a certain table + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->update('users', 'u') + * ->set('u.password', md5('password')) + * ->where('u.id = ?'); + * </code> + * + * @param string $update The table whose rows are subject to the update. + * @param string $alias The table alias used in the constructed query. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function update($update = null, $alias = null) { + $this->queryBuilder->update( + $this->getTableName($update), + $alias + ); + + return $this; + } + + /** + * Turns the query being built into an insert query that inserts into + * a certain table + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->insert('users') + * ->values( + * array( + * 'name' => '?', + * 'password' => '?' + * ) + * ); + * </code> + * + * @param string $insert The table into which the rows should be inserted. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function insert($insert = null) { + $this->queryBuilder->insert( + $this->getTableName($insert) + ); + + $this->lastInsertedTable = $insert; + + return $this; + } + + /** + * Creates and adds a query root corresponding to the table identified by the + * given alias, forming a cartesian product with any existing query roots. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u.id') + * ->from('users', 'u') + * </code> + * + * @param string $from The table. + * @param string|null $alias The alias of the table. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function from($from, $alias = null) { + $this->queryBuilder->from( + $this->getTableName($from), + $alias + ); + + return $this; + } + + /** + * Creates and adds a join to the query. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * </code> + * + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string $condition The condition for the join. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function join($fromAlias, $join, $alias, $condition = null) { + $this->queryBuilder->join( + $fromAlias, + $this->getTableName($join), + $alias, + $condition + ); + + return $this; + } + + /** + * Creates and adds a join to the query. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * </code> + * + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string $condition The condition for the join. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function innerJoin($fromAlias, $join, $alias, $condition = null) { + $this->queryBuilder->innerJoin( + $fromAlias, + $this->getTableName($join), + $alias, + $condition + ); + + return $this; + } + + /** + * Creates and adds a left join to the query. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * </code> + * + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string $condition The condition for the join. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function leftJoin($fromAlias, $join, $alias, $condition = null) { + $this->queryBuilder->leftJoin( + $fromAlias, + $this->getTableName($join), + $alias, + $condition + ); + + return $this; + } + + /** + * Creates and adds a right join to the query. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * </code> + * + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string $condition The condition for the join. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function rightJoin($fromAlias, $join, $alias, $condition = null) { + $this->queryBuilder->rightJoin( + $fromAlias, + $this->getTableName($join), + $alias, + $condition + ); + + return $this; + } + + /** + * Sets a new value for a column in a bulk update query. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->update('users', 'u') + * ->set('u.password', md5('password')) + * ->where('u.id = ?'); + * </code> + * + * @param string $key The column to set. + * @param string $value The value, expression, placeholder, etc. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function set($key, $value) { + $this->queryBuilder->set( + $this->helper->quoteColumnName($key), + $this->helper->quoteColumnName($value) + ); + + return $this; + } + + /** + * Specifies one or more restrictions to the query result. + * Replaces any previously specified restrictions, if any. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->where('u.id = ?'); + * + * // You can optionally programatically build and/or expressions + * $qb = $conn->getQueryBuilder(); + * + * $or = $qb->expr()->orx(); + * $or->add($qb->expr()->eq('u.id', 1)); + * $or->add($qb->expr()->eq('u.id', 2)); + * + * $qb->update('users', 'u') + * ->set('u.password', md5('password')) + * ->where($or); + * </code> + * + * @param mixed $predicates The restriction predicates. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function where($predicates) { + call_user_func_array( + [$this->queryBuilder, 'where'], + func_get_args() + ); + + return $this; + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * conjunction with any previously specified restrictions. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where('u.username LIKE ?') + * ->andWhere('u.is_active = 1'); + * </code> + * + * @param mixed $where The query restrictions. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + * + * @see where() + */ + public function andWhere($where) { + call_user_func_array( + [$this->queryBuilder, 'andWhere'], + func_get_args() + ); + + return $this; + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * disjunction with any previously specified restrictions. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->where('u.id = 1') + * ->orWhere('u.id = 2'); + * </code> + * + * @param mixed $where The WHERE statement. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + * + * @see where() + */ + public function orWhere($where) { + call_user_func_array( + [$this->queryBuilder, 'orWhere'], + func_get_args() + ); + + return $this; + } + + /** + * Specifies a grouping over the results of the query. + * Replaces any previously specified groupings, if any. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->groupBy('u.id'); + * </code> + * + * @param mixed $groupBy The grouping expression. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function groupBy($groupBy) { + $groupBys = is_array($groupBy) ? $groupBy : func_get_args(); + + call_user_func_array( + [$this->queryBuilder, 'groupBy'], + $this->helper->quoteColumnNames($groupBys) + ); + + return $this; + } + + /** + * Adds a grouping expression to the query. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->groupBy('u.lastLogin'); + * ->addGroupBy('u.createdAt') + * </code> + * + * @param mixed $groupBy The grouping expression. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function addGroupBy($groupBy) { + $groupBys = is_array($groupBy) ? $groupBy : func_get_args(); + + call_user_func_array( + [$this->queryBuilder, 'addGroupBy'], + $this->helper->quoteColumnNames($groupBys) + ); + + return $this; + } + + /** + * Sets a value for a column in an insert query. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->insert('users') + * ->values( + * array( + * 'name' => '?' + * ) + * ) + * ->setValue('password', '?'); + * </code> + * + * @param string $column The column into which the value should be inserted. + * @param string $value The value that should be inserted into the column. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function setValue($column, $value) { + $this->queryBuilder->setValue( + $this->helper->quoteColumnName($column), + $value + ); + + return $this; + } + + /** + * Specifies values for an insert query indexed by column names. + * Replaces any previous values, if any. + * + * <code> + * $qb = $conn->getQueryBuilder() + * ->insert('users') + * ->values( + * array( + * 'name' => '?', + * 'password' => '?' + * ) + * ); + * </code> + * + * @param array $values The values to specify for the insert query indexed by column names. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function values(array $values) { + $quotedValues = []; + foreach ($values as $key => $value) { + $quotedValues[$this->helper->quoteColumnName($key)] = $value; + } + + $this->queryBuilder->values($quotedValues); + + return $this; + } + + /** + * Specifies a restriction over the groups of the query. + * Replaces any previous having restrictions, if any. + * + * @param mixed $having The restriction over the groups. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function having($having) { + call_user_func_array( + [$this->queryBuilder, 'having'], + func_get_args() + ); + + return $this; + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * conjunction with any existing having restrictions. + * + * @param mixed $having The restriction to append. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function andHaving($having) { + call_user_func_array( + [$this->queryBuilder, 'andHaving'], + func_get_args() + ); + + return $this; + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * disjunction with any existing having restrictions. + * + * @param mixed $having The restriction to add. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function orHaving($having) { + call_user_func_array( + [$this->queryBuilder, 'orHaving'], + func_get_args() + ); + + return $this; + } + + /** + * Specifies an ordering for the query results. + * Replaces any previously specified orderings, if any. + * + * @param string $sort The ordering expression. + * @param string $order The ordering direction. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function orderBy($sort, $order = null) { + $this->queryBuilder->orderBy( + $this->helper->quoteColumnName($sort), + $order + ); + + return $this; + } + + /** + * Adds an ordering to the query results. + * + * @param string $sort The ordering expression. + * @param string $order The ordering direction. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function addOrderBy($sort, $order = null) { + $this->queryBuilder->addOrderBy( + $this->helper->quoteColumnName($sort), + $order + ); + + return $this; + } + + /** + * Gets a query part by its name. + * + * @param string $queryPartName + * + * @return mixed + */ + public function getQueryPart($queryPartName) { + return $this->queryBuilder->getQueryPart($queryPartName); + } + + /** + * Gets all query parts. + * + * @return array + */ + public function getQueryParts() { + return $this->queryBuilder->getQueryParts(); + } + + /** + * Resets SQL parts. + * + * @param array|null $queryPartNames + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function resetQueryParts($queryPartNames = null) { + $this->queryBuilder->resetQueryParts($queryPartNames); + + return $this; + } + + /** + * Resets a single SQL part. + * + * @param string $queryPartName + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder This QueryBuilder instance. + */ + public function resetQueryPart($queryPartName) { + $this->queryBuilder->resetQueryPart($queryPartName); + + return $this; + } + + /** + * Creates a new named parameter and bind the value $value to it. + * + * This method provides a shortcut for PDOStatement::bindValue + * when using prepared statements. + * + * The parameter $value specifies the value that you want to bind. If + * $placeholder is not provided bindValue() will automatically create a + * placeholder for you. An automatic placeholder will be of the name + * ':dcValue1', ':dcValue2' etc. + * + * For more information see {@link http://php.net/pdostatement-bindparam} + * + * Example: + * <code> + * $value = 2; + * $q->eq( 'id', $q->bindValue( $value ) ); + * $stmt = $q->executeQuery(); // executed with 'id = 2' + * </code> + * + * @license New BSD License + * @link http://www.zetacomponents.org + * + * @param mixed $value + * @param mixed $type + * @param string $placeHolder The name to bind with. The string must start with a colon ':'. + * + * @return IParameter the placeholder name used. + */ + public function createNamedParameter($value, $type = IQueryBuilder::PARAM_STR, $placeHolder = null) { + return new Parameter($this->queryBuilder->createNamedParameter($value, $type, $placeHolder)); + } + + /** + * Creates a new positional parameter and bind the given value to it. + * + * Attention: If you are using positional parameters with the query builder you have + * to be very careful to bind all parameters in the order they appear in the SQL + * statement , otherwise they get bound in the wrong order which can lead to serious + * bugs in your code. + * + * Example: + * <code> + * $qb = $conn->getQueryBuilder(); + * $qb->select('u.*') + * ->from('users', 'u') + * ->where('u.username = ' . $qb->createPositionalParameter('Foo', IQueryBuilder::PARAM_STR)) + * ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', IQueryBuilder::PARAM_STR)) + * </code> + * + * @param mixed $value + * @param integer $type + * + * @return IParameter + */ + public function createPositionalParameter($value, $type = IQueryBuilder::PARAM_STR) { + return new Parameter($this->queryBuilder->createPositionalParameter($value, $type)); + } + + /** + * Creates a new parameter + * + * Example: + * <code> + * $qb = $conn->getQueryBuilder(); + * $qb->select('u.*') + * ->from('users', 'u') + * ->where('u.username = ' . $qb->createParameter('name')) + * ->setParameter('name', 'Bar', IQueryBuilder::PARAM_STR)) + * </code> + * + * @param string $name + * + * @return IParameter + */ + public function createParameter($name) { + return new Parameter(':' . $name); + } + + /** + * Creates a new function + * + * Attention: Column names inside the call have to be quoted before hand + * + * Example: + * <code> + * $qb = $conn->getQueryBuilder(); + * $qb->select($qb->createFunction('COUNT(*)')) + * ->from('users', 'u') + * echo $qb->getSQL(); // SELECT COUNT(*) FROM `users` u + * </code> + * <code> + * $qb = $conn->getQueryBuilder(); + * $qb->select($qb->createFunction('COUNT(`column`)')) + * ->from('users', 'u') + * echo $qb->getSQL(); // SELECT COUNT(`column`) FROM `users` u + * </code> + * + * @param string $call + * + * @return IQueryFunction + */ + public function createFunction($call) { + return new QueryFunction($call); + } + + /** + * Used to get the id of the last inserted element + * @return int + * @throws \BadMethodCallException When being called before an insert query has been run. + */ + public function getLastInsertId() { + if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::INSERT && $this->lastInsertedTable) { + // lastInsertId() needs the prefix but no quotes + $table = $this->prefixTableName($this->lastInsertedTable); + return (int) $this->connection->lastInsertId($table); + } + + throw new \BadMethodCallException('Invalid call to getLastInsertId without using insert() before.'); + } + + /** + * Returns the table name quoted and with database prefix as needed by the implementation + * + * @param string $table + * @return string + */ + public function getTableName($table) { + $table = $this->prefixTableName($table); + return $this->helper->quoteColumnName($table); + } + + /** + * Returns the table name with database prefix as needed by the implementation + * + * @param string $table + * @return string + */ + protected function prefixTableName($table) { + if ($this->automaticTablePrefix === false || strpos($table, '*PREFIX*') === 0) { + return $table; + } + + return '*PREFIX*' . $table; + } + + /** + * Returns the column name quoted and with table alias prefix as needed by the implementation + * + * @param string $column + * @param string $tableAlias + * @return string + */ + public function getColumnName($column, $tableAlias = '') { + if ($tableAlias !== '') { + $tableAlias .= '.'; + } + + return $this->helper->quoteColumnName($tableAlias . $column); + } +} diff --git a/lib/private/DB/QueryBuilder/QueryFunction.php b/lib/private/DB/QueryBuilder/QueryFunction.php new file mode 100644 index 00000000000..ac6d73f3cbf --- /dev/null +++ b/lib/private/DB/QueryBuilder/QueryFunction.php @@ -0,0 +1,40 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB\QueryBuilder; + +use OCP\DB\QueryBuilder\IQueryFunction; + +class QueryFunction implements IQueryFunction { + /** @var string */ + protected $function; + + public function __construct($function) { + $this->function = $function; + } + + /** + * @return string + */ + public function __toString() { + return (string) $this->function; + } +} diff --git a/lib/private/DB/QueryBuilder/QuoteHelper.php b/lib/private/DB/QueryBuilder/QuoteHelper.php new file mode 100644 index 00000000000..fda243a3786 --- /dev/null +++ b/lib/private/DB/QueryBuilder/QuoteHelper.php @@ -0,0 +1,75 @@ +<?php +/** + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB\QueryBuilder; + +use OCP\DB\QueryBuilder\ILiteral; +use OCP\DB\QueryBuilder\IParameter; +use OCP\DB\QueryBuilder\IQueryFunction; + +class QuoteHelper { + /** + * @param array|string|ILiteral|IParameter|IQueryFunction $strings string, Literal or Parameter + * @return array|string + */ + public function quoteColumnNames($strings) { + if (!is_array($strings)) { + return $this->quoteColumnName($strings); + } + + $return = []; + foreach ($strings as $string) { + $return[] = $this->quoteColumnName($string); + } + + return $return; + } + + /** + * @param string|ILiteral|IParameter|IQueryFunction $string string, Literal or Parameter + * @return string + */ + public function quoteColumnName($string) { + if ($string instanceof IParameter || $string instanceof ILiteral || $string instanceof IQueryFunction) { + return (string) $string; + } + + if ($string === null || $string === 'null' || $string === '*') { + return $string; + } + + if (!is_string($string)) { + throw new \InvalidArgumentException('Only strings, Literals and Parameters are allowed'); + } + + if (substr_count($string, '.')) { + list($alias, $columnName) = explode('.', $string, 2); + + if ($columnName === '*') { + return $string; + } + + return $alias . '.`' . $columnName . '`'; + } + + return '`' . $string . '`'; + } +} diff --git a/lib/private/DB/SQLiteMigrator.php b/lib/private/DB/SQLiteMigrator.php new file mode 100644 index 00000000000..8ea32581011 --- /dev/null +++ b/lib/private/DB/SQLiteMigrator.php @@ -0,0 +1,81 @@ +<?php +/** + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Victor Dubiniuk <dubiniuk@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Schema\Schema; + +class SQLiteMigrator extends Migrator { + + /** + * @param \Doctrine\DBAL\Schema\Schema $targetSchema + * @throws \OC\DB\MigrationException + * + * For sqlite we simple make a copy of the entire database, and test the migration on that + */ + public function checkMigrate(\Doctrine\DBAL\Schema\Schema $targetSchema) { + $dbFile = $this->connection->getDatabase(); + $tmpFile = $this->buildTempDatabase(); + copy($dbFile, $tmpFile); + + $connectionParams = array( + 'path' => $tmpFile, + 'driver' => 'pdo_sqlite', + ); + $conn = \Doctrine\DBAL\DriverManager::getConnection($connectionParams); + try { + $this->applySchema($targetSchema, $conn); + $conn->close(); + unlink($tmpFile); + } catch (DBALException $e) { + $conn->close(); + unlink($tmpFile); + throw new MigrationException('', $e->getMessage()); + } + } + + /** + * @return string + */ + private function buildTempDatabase() { + $dataDir = $this->config->getSystemValue("datadirectory", \OC::$SERVERROOT . '/data'); + $tmpFile = uniqid("oc_"); + return "$dataDir/$tmpFile.db"; + } + + /** + * @param Schema $targetSchema + * @param \Doctrine\DBAL\Connection $connection + * @return \Doctrine\DBAL\Schema\SchemaDiff + */ + protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) { + $platform = $connection->getDatabasePlatform(); + $platform->registerDoctrineTypeMapping('tinyint unsigned', 'integer'); + $platform->registerDoctrineTypeMapping('smallint unsigned', 'integer'); + $platform->registerDoctrineTypeMapping('varchar ', 'string'); + + return parent::getDiff($targetSchema, $connection); + } +} diff --git a/lib/private/DB/SQLiteSessionInit.php b/lib/private/DB/SQLiteSessionInit.php new file mode 100644 index 00000000000..0683c47d08e --- /dev/null +++ b/lib/private/DB/SQLiteSessionInit.php @@ -0,0 +1,65 @@ +<?php +/** + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\Event\ConnectionEventArgs; +use Doctrine\DBAL\Events; +use Doctrine\Common\EventSubscriber; + +class SQLiteSessionInit implements EventSubscriber { + /** + * @var bool + */ + private $caseSensitiveLike; + + /** + * @var string + */ + private $journalMode; + + /** + * Configure case sensitive like for each connection + * + * @param bool $caseSensitiveLike + * @param string $journalMode + */ + public function __construct($caseSensitiveLike, $journalMode) { + $this->caseSensitiveLike = $caseSensitiveLike; + $this->journalMode = $journalMode; + } + + /** + * @param ConnectionEventArgs $args + * @return void + */ + public function postConnect(ConnectionEventArgs $args) { + $sensitive = ($this->caseSensitiveLike) ? 'true' : 'false'; + $args->getConnection()->executeUpdate('PRAGMA case_sensitive_like = ' . $sensitive); + $args->getConnection()->executeUpdate('PRAGMA journal_mode = ' . $this->journalMode); + } + + public function getSubscribedEvents() { + return array(Events::postConnect); + } +} |