migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Jonny007-MKD <1-23-4-5@web.de>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Ole Ostergaard <ole.c.ostergaard@gmail.com>
* @author Ole Ostergaard <ole.ostergaard@knime.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @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\Exception;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use OC\DB\Exceptions\DbalException;
/**
* 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
* @throws Exception
*/
public function lastInsertId($table) {
return (int) $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;
}
/**
* Create an exclusive read+write lock on a table
*
* @param string $tableName
* @throws Exception
* @since 9.1.0
*/
public function lockTable($tableName) {
$this->conn->beginTransaction();
$this->conn->executeUpdate('LOCK TABLE `' .$tableName . '` IN EXCLUSIVE MODE');
}
/**
* Release a previous acquired lock again
*
* @throws Exception
* @since 9.1.0
*/
public function unlockTable() {
$this->conn->commit();
}
/**
* Insert a row if the matching row does not exists. To accomplish proper race condition avoidance
* it is needed that there is also a unique constraint on the values. Then this method will
* catch the exception and return 0.
*
* @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 Exception
* @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
*/
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, -5);
$query .= ' HAVING COUNT(*) = 0';
try {
return $this->conn->executeUpdate($query, $inserts);
} catch (UniqueConstraintViolationException $e) {
// if this is thrown then a concurrent insert happened between the insert and the sub-select in the insert, that should have avoided it
// it's fine to ignore this then
//
// more discussions about this can be found at https://github.com/nextcloud/server/pull/12315
return 0;
}
}
/**
* @throws \OCP\DB\Exception
*/
public function insertIgnoreConflict(string $table, array $values) : int {
try {
$builder = $this->conn->getQueryBuilder();
$builder->insert($table);
foreach ($values as $key => $value) {
$builder->setValue($key, $builder->createNamedParameter($value));
}
return $builder->executeStatement();
} catch (DbalException $e) {
if ($e->getReason() === \OCP\DB\Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
return 0;
}
throw $e;
}
}
}
@@ -0,0 +1,53 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @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 {
/** @var string */
protected $collation;
/**
* @param string $tableName
*/
public function lockTable($tableName) {
$this->conn->executeUpdate('LOCK TABLES `' .$tableName . '` WRITE');
}
public function unlockTable() {
$this->conn->executeUpdate('UNLOCK TABLES');
}
public function fixupStatement($statement) {
$statement = str_replace(' ILIKE ', ' COLLATE ' . $this->getCollation() . ' LIKE ', $statement);
return $statement;
}
protected function getCollation(): string {
if (!$this->collation) {
$params = $this->conn->getParams();
$this->collation = $params['collation'] ?? (($params['charset'] ?? 'utf8') . '_general_ci');
}
return $this->collation;
}
}
@@ -0,0 +1,48 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Robin Appelman <robin@icewind.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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);
}
public const UNIX_TIMESTAMP_REPLACEMENT = "(cast(sys_extract_utc(systimestamp) as date) - date'1970-01-01') * 86400";
public function fixupStatement($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;
}
}
@@ -0,0 +1,74 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Ole Ostergaard <ole.c.ostergaard@gmail.com>
* @author Ole Ostergaard <ole.ostergaard@knime.com>
*
* @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 {
protected $compatModePre9_5 = null;
public function lastInsertId($table) {
$result = $this->conn->executeQuery('SELECT lastval()');
$val = $result->fetchOne();
$result->free();
return (int)$val;
}
public 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;
}
public function insertIgnoreConflict(string $table, array $values) : int {
if ($this->isPre9_5CompatMode() === true) {
return parent::insertIgnoreConflict($table, $values);
}
// "upsert" is only available since PgSQL 9.5, but the generic way
// would leave error logs in the DB.
$builder = $this->conn->getQueryBuilder();
$builder->insert($table);
foreach ($values as $key => $value) {
$builder->setValue($key, $builder->createNamedParameter($value));
}
$queryString = $builder->getSQL() . ' ON CONFLICT DO NOTHING';
return $this->conn->executeUpdate($queryString, $builder->getParameters(), $builder->getParameterTypes());
}
protected function isPre9_5CompatMode(): bool {
if ($this->compatModePre9_5 !== null) {
return $this->compatModePre9_5;
}
$result = $this->conn->executeQuery('SHOW SERVER_VERSION');
$version = $result->fetchOne();
$result->free();
$this->compatModePre9_5 = version_compare($version, '9.5', '<');
return $this->compatModePre9_5;
}
}
@@ -0,0 +1,98 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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\Exception\UniqueConstraintViolationException;
class AdapterSqlite extends Adapter {
/**
* @param string $tableName
*/
public function lockTable($tableName) {
$this->conn->executeUpdate('BEGIN EXCLUSIVE TRANSACTION');
}
public function unlockTable() {
$this->conn->executeUpdate('COMMIT TRANSACTION');
}
public function fixupStatement($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('GREATEST(', 'MAX(', $statement);
$statement = str_ireplace('UNIX_TIMESTAMP()', 'strftime(\'%s\',\'now\')', $statement);
return $statement;
}
/**
* Insert a row if the matching row does not exists. To accomplish proper race condition avoidance
* it is needed that there is also a unique constraint on the values. Then this method will
* catch the exception and return 0.
*
* @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\Exception
* @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
*/
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, -5);
$query .= ')';
try {
return $this->conn->executeUpdate($query, $inserts);
} catch (UniqueConstraintViolationException $e) {
// if this is thrown then a concurrent insert happened between the insert and the sub-select in the insert, that should have avoided it
// it's fine to ignore this then
//
// more discussions about this can be found at https://github.com/nextcloud/server/pull/12315
return 0;
}
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB;
use Doctrine\DBAL\Logging\DebugStack;
class BacktraceDebugStack extends DebugStack {
public function startQuery($sql, ?array $params = null, ?array $types = null) {
parent::startQuery($sql, $params, $types);
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$this->queries[$this->currentQuery]['backtrace'] = $backtrace;
$this->queries[$this->currentQuery]['start'] = $this->start;
}
}
+606
View File
@@ -0,0 +1,606 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Ole Ostergaard <ole.c.ostergaard@gmail.com>
* @author Ole Ostergaard <ole.ostergaard@knime.com>
* @author Philipp Schaffrath <github@philipp.schaffrath.email>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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\Common\EventManager;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Statement;
use OC\DB\QueryBuilder\QueryBuilder;
use OC\SystemConfig;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Diagnostics\IEventLogger;
use OCP\IRequestId;
use OCP\PreConditionNotMetException;
use OCP\Profiler\IProfiler;
use Psr\Log\LoggerInterface;
class Connection extends \Doctrine\DBAL\Connection {
/** @var string */
protected $tablePrefix;
/** @var \OC\DB\Adapter $adapter */
protected $adapter;
/** @var SystemConfig */
private $systemConfig;
private LoggerInterface $logger;
protected $lockedTable = null;
/** @var int */
protected $queriesBuilt = 0;
/** @var int */
protected $queriesExecuted = 0;
/** @var DbDataCollector|null */
protected $dbDataCollector = null;
/**
* Initializes a new instance of the Connection class.
*
* @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');
}
/**
* @psalm-suppress InternalMethod
*/
parent::__construct($params, $driver, $config, $eventManager);
$this->adapter = new $params['adapter']($this);
$this->tablePrefix = $params['tablePrefix'];
$this->systemConfig = \OC::$server->getSystemConfig();
$this->logger = \OC::$server->get(LoggerInterface::class);
/** @var \OCP\Profiler\IProfiler */
$profiler = \OC::$server->get(IProfiler::class);
if ($profiler->isEnabled()) {
$this->dbDataCollector = new DbDataCollector($this);
$profiler->add($this->dbDataCollector);
$debugStack = new BacktraceDebugStack();
$this->dbDataCollector->setDebugStack($debugStack);
$this->_config->setSQLLogger($debugStack);
}
}
/**
* @throws Exception
*/
public function connect() {
try {
if ($this->_conn) {
/** @psalm-suppress InternalMethod */
return parent::connect();
}
// Only trigger the event logger for the initial connect call
$eventLogger = \OC::$server->get(IEventLogger::class);
$eventLogger->start('connect:db', 'db connection opened');
/** @psalm-suppress InternalMethod */
$status = parent::connect();
$eventLogger->end('connect:db');
return $status;
} catch (Exception $e) {
// throw a new exception to prevent leaking info from the stacktrace
throw new Exception('Failed to connect to the database: ' . $e->getMessage(), $e->getCode());
}
}
public function getStats(): array {
return [
'built' => $this->queriesBuilt,
'executed' => $this->queriesExecuted,
];
}
/**
* Returns a QueryBuilder for the connection.
*/
public function getQueryBuilder(): IQueryBuilder {
$this->queriesBuilt++;
return new QueryBuilder(
new ConnectionAdapter($this),
$this->systemConfig,
$this->logger
);
}
/**
* Gets the QueryBuilder for the connection.
*
* @return \Doctrine\DBAL\Query\QueryBuilder
* @deprecated please use $this->getQueryBuilder() instead
*/
public function createQueryBuilder() {
$backtrace = $this->getCallerBacktrace();
$this->logger->debug('Doctrine QueryBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
$this->queriesBuilt++;
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();
$this->logger->debug('Doctrine ExpressionBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
$this->queriesBuilt++;
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;
}
/**
* Prepares an SQL statement.
*
* @param string $statement The SQL statement to prepare.
* @param int|null $limit
* @param int|null $offset
*
* @return Statement The prepared statement.
* @throws Exception
*/
public function prepare($sql, $limit = null, $offset = null): Statement {
if ($limit === -1 || $limit === null) {
$limit = null;
} else {
$limit = (int) $limit;
}
if ($offset !== null) {
$offset = (int) $offset;
}
if (!is_null($limit)) {
$platform = $this->getDatabasePlatform();
$sql = $platform->modifyLimitQuery($sql, $limit, $offset);
}
$statement = $this->replaceTablePrefix($sql);
$statement = $this->adapter->fixupStatement($statement);
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 $sql 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 Result The executed statement.
*
* @throws \Doctrine\DBAL\Exception
*/
public function executeQuery(string $sql, array $params = [], $types = [], QueryCacheProfile $qcp = null): Result {
$sql = $this->replaceTablePrefix($sql);
$sql = $this->adapter->fixupStatement($sql);
$this->queriesExecuted++;
$this->logQueryToFile($sql);
return parent::executeQuery($sql, $params, $types, $qcp);
}
/**
* @throws Exception
*/
public function executeUpdate(string $sql, array $params = [], array $types = []): int {
$sql = $this->replaceTablePrefix($sql);
$sql = $this->adapter->fixupStatement($sql);
$this->queriesExecuted++;
$this->logQueryToFile($sql);
return parent::executeUpdate($sql, $params, $types);
}
/**
* 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 $sql The SQL query.
* @param array $params The query parameters.
* @param array $types The parameter types.
*
* @return int The number of affected rows.
*
* @throws \Doctrine\DBAL\Exception
*/
public function executeStatement($sql, array $params = [], array $types = []): int {
$sql = $this->replaceTablePrefix($sql);
$sql = $this->adapter->fixupStatement($sql);
$this->queriesExecuted++;
$this->logQueryToFile($sql);
return (int)parent::executeStatement($sql, $params, $types);
}
protected function logQueryToFile(string $sql): void {
$logFile = $this->systemConfig->getValue('query_log_file');
if ($logFile !== '' && is_writable(dirname($logFile)) && (!file_exists($logFile) || is_writable($logFile))) {
$prefix = '';
if ($this->systemConfig->getValue('query_log_file_requestid') === 'yes') {
$prefix .= \OC::$server->get(IRequestId::class)->getId() . "\t";
}
file_put_contents(
$this->systemConfig->getValue('query_log_file', ''),
$prefix . $sql . "\n",
FILE_APPEND
);
}
}
/**
* 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 int the last inserted ID.
* @throws Exception
*/
public function lastInsertId($name = null): int {
if ($name) {
$name = $this->replaceTablePrefix($name);
}
return $this->adapter->lastInsertId($name);
}
/**
* @internal
* @throws Exception
*/
public function realLastInsertId($seqName = null) {
return parent::lastInsertId($seqName);
}
/**
* Insert a row if the matching row does not exists. To accomplish proper race condition avoidance
* it is needed that there is also a unique constraint on the values. Then this method will
* catch the exception and return 0.
*
* @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\Exception
* @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
*/
public function insertIfNotExist($table, $input, array $compare = null) {
return $this->adapter->insertIfNotExist($table, $input, $compare);
}
public function insertIgnoreConflict(string $table, array $values) : int {
return $this->adapter->insertIgnoreConflict($table, $values);
}
private function getType($value) {
if (is_bool($value)) {
return IQueryBuilder::PARAM_BOOL;
} elseif (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 \OCP\DB\Exception
* @throws PreConditionNotMetException
*/
public function setValues(string $table, array $keys, array $values, array $updatePreconditionValues = []): int {
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->executeStatement();
} catch (\OCP\DB\Exception $e) {
if (!in_array($e->getReason(), [
\OCP\DB\Exception::REASON_CONSTRAINT_VIOLATION,
\OCP\DB\Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION,
])
) {
throw $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) {
if ($value === '') {
$where->add($updateQb->expr()->emptyString(
$name
));
} else {
$where->add($updateQb->expr()->eq(
$name,
$updateQb->createNamedParameter($value, $this->getType($value)),
$this->getType($value)
));
}
}
$updateQb->where($where);
$affected = $updateQb->executeStatement();
if ($affected === 0 && !empty($updatePreconditionValues)) {
throw new PreConditionNotMetException();
}
return 0;
}
}
/**
* Create an exclusive read+write lock on a table
*
* @param string $tableName
*
* @throws \BadMethodCallException When trying to acquire a second lock
* @throws Exception
* @since 9.1.0
*/
public function lockTable($tableName) {
if ($this->lockedTable !== null) {
throw new \BadMethodCallException('Can not lock a new table until the previous lock is released.');
}
$tableName = $this->tablePrefix . $tableName;
$this->lockedTable = $tableName;
$this->adapter->lockTable($tableName);
}
/**
* Release a previous acquired lock again
*
* @throws Exception
* @since 9.1.0
*/
public function unlockTable() {
$this->adapter->unlockTable();
$this->lockedTable = null;
}
/**
* 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 (!empty($errorInfo)) {
$msg .= 'SQLSTATE = '.$errorInfo[0] . ', ';
$msg .= 'Driver Code = '.$errorInfo[1] . ', ';
$msg .= 'Driver Message = '.$errorInfo[2];
}
return $msg;
}
public function errorCode() {
return -1;
}
public function errorInfo() {
return [];
}
/**
* Drop a table from the database if it exists
*
* @param string $table table name without the prefix
*
* @throws Exception
*/
public function dropTable($table) {
$table = $this->tablePrefix . trim($table);
$schema = $this->getSchemaManager();
if ($schema->tablesExist([$table])) {
$schema->dropTable($table);
}
}
/**
* Check if a table exists
*
* @param string $table table name without the prefix
*
* @return bool
* @throws Exception
*/
public function tableExists($table) {
$table = $this->tablePrefix . trim($table);
$schema = $this->getSchemaManager();
return $schema->tablesExist([$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;
}
/**
* Escape a parameter to be used in a LIKE query
*
* @param string $param
* @return string
*/
public function escapeLikeParameter($param) {
return addcslashes($param, '\\_%');
}
/**
* Check whether or not the current database support 4byte wide unicode
*
* @return bool
* @since 11.0.0
*/
public function supports4ByteText() {
if (!$this->getDatabasePlatform() instanceof MySQLPlatform) {
return true;
}
return $this->getParams()['charset'] === 'utf8mb4';
}
/**
* Create the schema of the connected database
*
* @return Schema
* @throws Exception
*/
public function createSchema() {
$migrator = $this->getMigrator();
return $migrator->createSchema();
}
/**
* Migrate the database to the given schema
*
* @param Schema $toSchema
* @param bool $dryRun If true, will return the sql queries instead of running them.
*
* @throws Exception
*
* @return string|null Returns a string only if $dryRun is true.
*/
public function migrateToSchema(Schema $toSchema, bool $dryRun = false) {
$migrator = $this->getMigrator();
if ($dryRun) {
return $migrator->generateChangeScript($toSchema);
} else {
$migrator->migrate($toSchema);
}
}
private function getMigrator() {
// TODO properly inject those dependencies
$random = \OC::$server->getSecureRandom();
$platform = $this->getDatabasePlatform();
$config = \OC::$server->getConfig();
$dispatcher = \OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class);
if ($platform instanceof SqlitePlatform) {
return new SQLiteMigrator($this, $config, $dispatcher);
} elseif ($platform instanceof OraclePlatform) {
return new OracleMigrator($this, $config, $dispatcher);
} else {
return new Migrator($this, $config, $dispatcher);
}
}
}
@@ -0,0 +1,264 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\DBAL\Schema\Schema;
use OC\DB\Exceptions\DbalException;
use OCP\DB\IPreparedStatement;
use OCP\DB\IResult;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
/**
* Adapts the public API to our internal DBAL connection wrapper
*/
class ConnectionAdapter implements IDBConnection {
/** @var Connection */
private $inner;
public function __construct(Connection $inner) {
$this->inner = $inner;
}
public function getQueryBuilder(): IQueryBuilder {
return $this->inner->getQueryBuilder();
}
public function prepare($sql, $limit = null, $offset = null): IPreparedStatement {
try {
return new PreparedStatement(
$this->inner->prepare($sql, $limit, $offset)
);
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function executeQuery(string $sql, array $params = [], $types = []): IResult {
try {
return new ResultAdapter(
$this->inner->executeQuery($sql, $params, $types)
);
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function executeUpdate(string $sql, array $params = [], array $types = []): int {
try {
return $this->inner->executeUpdate($sql, $params, $types);
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function executeStatement($sql, array $params = [], array $types = []): int {
try {
return $this->inner->executeStatement($sql, $params, $types);
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function lastInsertId(string $table): int {
try {
return $this->inner->lastInsertId($table);
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function insertIfNotExist(string $table, array $input, array $compare = null) {
try {
return $this->inner->insertIfNotExist($table, $input, $compare);
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function insertIgnoreConflict(string $table, array $values): int {
try {
return $this->inner->insertIgnoreConflict($table, $values);
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function setValues($table, array $keys, array $values, array $updatePreconditionValues = []): int {
try {
return $this->inner->setValues($table, $keys, $values, $updatePreconditionValues);
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function lockTable($tableName): void {
try {
$this->inner->lockTable($tableName);
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function unlockTable(): void {
try {
$this->inner->unlockTable();
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function beginTransaction(): void {
try {
$this->inner->beginTransaction();
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function inTransaction(): bool {
return $this->inner->inTransaction();
}
public function commit(): void {
try {
$this->inner->commit();
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function rollBack(): void {
try {
$this->inner->rollBack();
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function getError(): string {
return $this->inner->getError();
}
public function errorCode() {
return $this->inner->errorCode();
}
public function errorInfo() {
return $this->inner->errorInfo();
}
public function connect(): bool {
try {
return $this->inner->connect();
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function close(): void {
$this->inner->close();
}
public function quote($input, $type = IQueryBuilder::PARAM_STR) {
return $this->inner->quote($input, $type);
}
/**
* @todo we are leaking a 3rdparty type here
*/
public function getDatabasePlatform(): AbstractPlatform {
return $this->inner->getDatabasePlatform();
}
public function dropTable(string $table): void {
try {
$this->inner->dropTable($table);
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function tableExists(string $table): bool {
try {
return $this->inner->tableExists($table);
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function escapeLikeParameter(string $param): string {
return $this->inner->escapeLikeParameter($param);
}
public function supports4ByteText(): bool {
return $this->inner->supports4ByteText();
}
/**
* @todo leaks a 3rdparty type
*/
public function createSchema(): Schema {
try {
return $this->inner->createSchema();
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function migrateToSchema(Schema $toSchema): void {
try {
$this->inner->migrateToSchema($toSchema);
} catch (Exception $e) {
throw DbalException::wrap($e);
}
}
public function getInner(): Connection {
return $this->inner;
}
public function getDatabaseProvider(): string {
$platform = $this->inner->getDatabasePlatform();
if ($platform instanceof MySQLPlatform) {
return IDBConnection::PLATFORM_MYSQL;
} elseif ($platform instanceof OraclePlatform) {
return IDBConnection::PLATFORM_ORACLE;
} elseif ($platform instanceof PostgreSQLPlatform) {
return IDBConnection::PLATFORM_POSTGRES;
} elseif ($platform instanceof SqlitePlatform) {
return IDBConnection::PLATFORM_SQLITE;
} else {
throw new \Exception('Database ' . $platform::class . ' not supported');
}
}
}
@@ -0,0 +1,265 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Andreas Fischer <bantu@owncloud.com>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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\Common\EventManager;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Event\Listeners\OracleSessionInit;
use Doctrine\DBAL\Event\Listeners\SQLSessionInit;
use OC\SystemConfig;
/**
* Takes care of creating and configuring Doctrine connections.
*/
class ConnectionFactory {
/** @var string default database name */
public const DEFAULT_DBNAME = 'owncloud';
/** @var string default database table prefix */
public const DEFAULT_DBTABLEPREFIX = 'oc_';
/**
* @var array
*
* Array mapping DBMS type to default connection parameters passed to
* \Doctrine\DBAL\DriverManager::getConnection().
*/
protected $defaultConnectionParams = [
'mysql' => [
'adapter' => AdapterMySQL::class,
'charset' => 'UTF8',
'driver' => 'pdo_mysql',
'wrapperClass' => Connection::class,
],
'oci' => [
'adapter' => AdapterOCI8::class,
'charset' => 'AL32UTF8',
'driver' => 'oci8',
'wrapperClass' => OracleConnection::class,
],
'pgsql' => [
'adapter' => AdapterPgSql::class,
'driver' => 'pdo_pgsql',
'wrapperClass' => Connection::class,
],
'sqlite3' => [
'adapter' => AdapterSqlite::class,
'driver' => 'pdo_sqlite',
'wrapperClass' => Connection::class,
],
];
/** @var SystemConfig */
private $config;
/**
* ConnectionFactory constructor.
*
* @param SystemConfig $systemConfig
*/
public function __construct(SystemConfig $systemConfig) {
$this->config = $systemConfig;
if ($this->config->getValue('mysql.utf8mb4', false)) {
$this->defaultConnectionParams['mysql']['charset'] = 'utf8mb4';
}
$collationOverride = $this->config->getValue('mysql.collation', null);
if ($collationOverride) {
$this->defaultConnectionParams['mysql']['collation'] = $collationOverride;
}
}
/**
* @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'] = [
\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 EventManager();
$eventManager->addEventSubscriber(new SetTransactionIsolationLevel());
switch ($normalizedType) {
case 'mysql':
$eventManager->addEventSubscriber(
new SQLSessionInit("SET SESSION AUTOCOMMIT=1"));
break;
case 'oci':
$eventManager->addEventSubscriber(new OracleSessionInit);
// the driverOptions are unused in dbal and need to be mapped to the parameters
if (isset($additionalConnectionParams['driverOptions'])) {
$additionalConnectionParams = array_merge($additionalConnectionParams, $additionalConnectionParams['driverOptions']);
}
$host = $additionalConnectionParams['host'];
$port = $additionalConnectionParams['port'] ?? null;
$dbName = $additionalConnectionParams['dbname'];
// we set the connect string as dbname and unset the host to coerce doctrine into using it as connect string
if ($host === '') {
$additionalConnectionParams['dbname'] = $dbName; // use dbname as easy connect name
} else {
$additionalConnectionParams['dbname'] = '//' . $host . (!empty($port) ? ":{$port}" : "") . '/' . $dbName;
}
unset($additionalConnectionParams['host']);
break;
case 'sqlite3':
$journalMode = $additionalConnectionParams['sqlite.journal_mode'];
$additionalConnectionParams['platform'] = new OCSqlitePlatform();
$eventManager->addEventSubscriber(new SQLiteSessionInit(true, $journalMode));
break;
}
/** @var Connection $connection */
$connection = DriverManager::getConnection(
array_merge($this->getDefaultConnectionParams($type), $additionalConnectionParams),
new 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;
}
/**
* Checks whether the specified DBMS type is valid.
*
* @param string $type
* @return bool
*/
public function isValidType($type) {
$normalizedType = $this->normalizeType($type);
return isset($this->defaultConnectionParams[$normalizedType]);
}
/**
* Create the connection parameters for the config
*
* @param string $configPrefix
* @return array
*/
public function createConnectionParams(string $configPrefix = '') {
$type = $this->config->getValue('dbtype', 'sqlite');
$connectionParams = [
'user' => $this->config->getValue($configPrefix . 'dbuser', $this->config->getValue('dbuser', '')),
'password' => $this->config->getValue($configPrefix . 'dbpassword', $this->config->getValue('dbpassword', '')),
];
$name = $this->config->getValue($configPrefix . 'dbname', $this->config->getValue('dbname', self::DEFAULT_DBNAME));
if ($this->normalizeType($type) === 'sqlite3') {
$dataDir = $this->config->getValue("datadirectory", \OC::$SERVERROOT . '/data');
$connectionParams['path'] = $dataDir . '/' . $name . '.db';
} else {
$host = $this->config->getValue($configPrefix . 'dbhost', $this->config->getValue('dbhost', ''));
$connectionParams = array_merge($connectionParams, $this->splitHostFromPortAndSocket($host));
$connectionParams['dbname'] = $name;
}
$connectionParams['tablePrefix'] = $this->config->getValue('dbtableprefix', self::DEFAULT_DBTABLEPREFIX);
$connectionParams['sqlite.journal_mode'] = $this->config->getValue('sqlite.journal_mode', 'WAL');
//additional driver options, eg. for mysql ssl
$driverOptions = $this->config->getValue($configPrefix . 'dbdriveroptions', $this->config->getValue('dbdriveroptions', null));
if ($driverOptions) {
$connectionParams['driverOptions'] = $driverOptions;
}
// set default table creation options
$connectionParams['defaultTableOptions'] = [
'collate' => 'utf8_bin',
'tablePrefix' => $connectionParams['tablePrefix']
];
if ($this->config->getValue('mysql.utf8mb4', false)) {
$connectionParams['defaultTableOptions'] = [
'collate' => 'utf8mb4_bin',
'charset' => 'utf8mb4',
'tablePrefix' => $connectionParams['tablePrefix']
];
}
if ($this->config->getValue('dbpersistent', false)) {
$connectionParams['persistent'] = true;
}
return $connectionParams;
}
/**
* @param string $host
* @return array
*/
protected function splitHostFromPortAndSocket($host): array {
$params = [
'host' => $host,
];
$matches = [];
if (preg_match('/^(.*):([^\]:]+)$/', $host, $matches)) {
// Host variable carries a port or socket.
$params['host'] = $matches[1];
if (is_numeric($matches[2])) {
$params['port'] = (int) $matches[2];
} else {
$params['unix_socket'] = $matches[2];
}
}
return $params;
}
}
@@ -0,0 +1,153 @@
<?php
declare(strict_types = 1);
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;
use OC\AppFramework\Http\Request;
use OCP\AppFramework\Http\Response;
class DbDataCollector extends \OCP\DataCollector\AbstractDataCollector {
protected ?BacktraceDebugStack $debugStack = null;
private Connection $connection;
/**
* DbDataCollector constructor.
*/
public function __construct(Connection $connection) {
$this->connection = $connection;
}
public function setDebugStack(BacktraceDebugStack $debugStack, $name = 'default'): void {
$this->debugStack = $debugStack;
}
/**
* @inheritDoc
*/
public function collect(Request $request, Response $response, \Throwable $exception = null): void {
$queries = $this->sanitizeQueries($this->debugStack->queries);
$this->data = [
'queries' => $queries,
];
}
public function getName(): string {
return 'db';
}
public function getQueries(): array {
return $this->data['queries'];
}
private function sanitizeQueries(array $queries): array {
foreach ($queries as $i => $query) {
$queries[$i] = $this->sanitizeQuery($query);
}
return $queries;
}
private function sanitizeQuery(array $query): array {
$query['explainable'] = true;
$query['runnable'] = true;
if (null === $query['params']) {
$query['params'] = [];
}
if (!\is_array($query['params'])) {
$query['params'] = [$query['params']];
}
if (!\is_array($query['types'])) {
$query['types'] = [];
}
foreach ($query['params'] as $j => $param) {
$e = null;
if (isset($query['types'][$j])) {
// Transform the param according to the type
$type = $query['types'][$j];
if (\is_string($type)) {
$type = Type::getType($type);
}
if ($type instanceof Type) {
$query['types'][$j] = $type->getBindingType();
try {
$param = $type->convertToDatabaseValue($param, $this->connection->getDatabasePlatform());
} catch (\TypeError $e) {
} catch (ConversionException $e) {
}
}
}
[$query['params'][$j], $explainable, $runnable] = $this->sanitizeParam($param, $e);
if (!$explainable) {
$query['explainable'] = false;
}
if (!$runnable) {
$query['runnable'] = false;
}
}
return $query;
}
/**
* Sanitizes a param.
*
* The return value is an array with the sanitized value and a boolean
* indicating if the original value was kept (allowing to use the sanitized
* value to explain the query).
*/
private function sanitizeParam($var, ?\Throwable $error): array {
if (\is_object($var)) {
return [$o = new ObjectParameter($var, $error), false, $o->isStringable() && !$error];
}
if ($error) {
return ['⚠ '.$error->getMessage(), false, false];
}
if (\is_array($var)) {
$a = [];
$explainable = $runnable = true;
foreach ($var as $k => $v) {
[$value, $e, $r] = $this->sanitizeParam($v, null);
$explainable = $explainable && $e;
$runnable = $runnable && $r;
$a[$k] = $value;
}
return [$a, $explainable, $runnable];
}
if (\is_resource($var)) {
return [sprintf('/* Resource(%s) */', get_resource_type($var)), false, false];
}
return [$var, true, true];
}
}
@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB\Exceptions;
use Doctrine\DBAL\ConnectionException;
use Doctrine\DBAL\Exception\ConstraintViolationException;
use Doctrine\DBAL\Exception\DatabaseObjectExistsException;
use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidArgumentException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\RetryableException;
use Doctrine\DBAL\Exception\ServerException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use OCP\DB\Exception;
/**
* Wrapper around the raw dbal exception, so we can pass it to apps that catch
* our OCP db exception
*
* @psalm-immutable
*/
class DbalException extends Exception {
/** @var \Doctrine\DBAL\Exception */
private $original;
/**
* @param \Doctrine\DBAL\Exception $original
* @param int $code
* @param string $message
*/
private function __construct(\Doctrine\DBAL\Exception $original, int $code, string $message) {
parent::__construct(
$message,
$code,
$original
);
$this->original = $original;
}
public static function wrap(\Doctrine\DBAL\Exception $original, string $message = ''): self {
return new self(
$original,
is_int($original->getCode()) ? $original->getCode() : 0,
empty($message) ? $original->getMessage() : $message
);
}
public function isRetryable(): bool {
return $this->original instanceof RetryableException;
}
public function getReason(): ?int {
/**
* Constraint errors
*/
if ($this->original instanceof ForeignKeyConstraintViolationException) {
return parent::REASON_FOREIGN_KEY_VIOLATION;
}
if ($this->original instanceof NotNullConstraintViolationException) {
return parent::REASON_NOT_NULL_CONSTRAINT_VIOLATION;
}
if ($this->original instanceof UniqueConstraintViolationException) {
return parent::REASON_UNIQUE_CONSTRAINT_VIOLATION;
}
// The base exception comes last
if ($this->original instanceof ConstraintViolationException) {
return parent::REASON_CONSTRAINT_VIOLATION;
}
/**
* Other server errors
*/
if ($this->original instanceof DatabaseObjectExistsException) {
return parent::REASON_DATABASE_OBJECT_EXISTS;
}
if ($this->original instanceof DatabaseObjectNotFoundException) {
return parent::REASON_DATABASE_OBJECT_NOT_FOUND;
}
if ($this->original instanceof DeadlockException) {
return parent::REASON_DEADLOCK;
}
if ($this->original instanceof InvalidFieldNameException) {
return parent::REASON_INVALID_FIELD_NAME;
}
if ($this->original instanceof NonUniqueFieldNameException) {
return parent::REASON_NON_UNIQUE_FIELD_NAME;
}
if ($this->original instanceof SyntaxErrorException) {
return parent::REASON_SYNTAX_ERROR;
}
// The base server exception class comes last
if ($this->original instanceof ServerException) {
return parent::REASON_SERVER;
}
/**
* Generic errors
*/
if ($this->original instanceof ConnectionException) {
return parent::REASON_CONNECTION_LOST;
}
if ($this->original instanceof InvalidArgumentException) {
return parent::REASON_INVALID_ARGUMENT;
}
if ($this->original instanceof DriverException) {
return parent::REASON_DRIVER;
}
return null;
}
}
@@ -0,0 +1,38 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin Appelman <robin@icewind.nl>
*
* @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;
}
}
@@ -0,0 +1,749 @@
<?php
/**
* @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
* @copyright Copyright (c) 2017, ownCloud GmbH
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
*
* @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\OraclePlatform;
use Doctrine\DBAL\Platforms\PostgreSQL94Platform;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Types;
use OC\App\InfoParser;
use OC\IntegrityCheck\Helpers\AppLocator;
use OC\Migration\SimpleOutput;
use OCP\AppFramework\App;
use OCP\AppFramework\QueryException;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IMigrationStep;
use OCP\Migration\IOutput;
use OCP\Server;
use Psr\Log\LoggerInterface;
class MigrationService {
private bool $migrationTableCreated;
private array $migrations;
private string $migrationsPath;
private string $migrationsNamespace;
private IOutput $output;
private LoggerInterface $logger;
private Connection $connection;
private string $appName;
private bool $checkOracle;
/**
* @throws \Exception
*/
public function __construct(string $appName, Connection $connection, ?IOutput $output = null, ?AppLocator $appLocator = null, ?LoggerInterface $logger = null) {
$this->appName = $appName;
$this->connection = $connection;
if ($logger === null) {
$this->logger = Server::get(LoggerInterface::class);
} else {
$this->logger = $logger;
}
if ($output === null) {
$this->output = new SimpleOutput($this->logger, $appName);
} else {
$this->output = $output;
}
if ($appName === 'core') {
$this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
$this->migrationsNamespace = 'OC\\Core\\Migrations';
$this->checkOracle = true;
} else {
if (null === $appLocator) {
$appLocator = new AppLocator();
}
$appPath = $appLocator->getAppPath($appName);
$namespace = App::buildAppNamespace($appName);
$this->migrationsPath = "$appPath/lib/Migration";
$this->migrationsNamespace = $namespace . '\\Migration';
$infoParser = new InfoParser();
$info = $infoParser->parse($appPath . '/appinfo/info.xml');
if (!isset($info['dependencies']['database'])) {
$this->checkOracle = true;
} else {
$this->checkOracle = false;
foreach ($info['dependencies']['database'] as $database) {
if (\is_string($database) && $database === 'oci') {
$this->checkOracle = true;
} elseif (\is_array($database) && isset($database['@value']) && $database['@value'] === 'oci') {
$this->checkOracle = true;
}
}
}
}
$this->migrationTableCreated = false;
}
/**
* Returns the name of the app for which this migration is executed
*/
public function getApp(): string {
return $this->appName;
}
/**
* @codeCoverageIgnore - this will implicitly tested on installation
*/
private function createMigrationTable(): bool {
if ($this->migrationTableCreated) {
return false;
}
if ($this->connection->tableExists('migrations') && \OC::$server->getConfig()->getAppValue('core', 'vendor', '') !== 'owncloud') {
$this->migrationTableCreated = true;
return false;
}
$schema = new SchemaWrapper($this->connection);
/**
* We drop the table when it has different columns or the definition does not
* match. E.g. ownCloud uses a length of 177 for app and 14 for version.
*/
try {
$table = $schema->getTable('migrations');
$columns = $table->getColumns();
if (count($columns) === 2) {
try {
$column = $table->getColumn('app');
$schemaMismatch = $column->getLength() !== 255;
if (!$schemaMismatch) {
$column = $table->getColumn('version');
$schemaMismatch = $column->getLength() !== 255;
}
} catch (SchemaException $e) {
// One of the columns is missing
$schemaMismatch = true;
}
if (!$schemaMismatch) {
// Table exists and schema matches: return back!
$this->migrationTableCreated = true;
return false;
}
}
// Drop the table, when it didn't match our expectations.
$this->connection->dropTable('migrations');
// Recreate the schema after the table was dropped.
$schema = new SchemaWrapper($this->connection);
} catch (SchemaException $e) {
// Table not found, no need to panic, we will create it.
}
$table = $schema->createTable('migrations');
$table->addColumn('app', Types::STRING, ['length' => 255]);
$table->addColumn('version', Types::STRING, ['length' => 255]);
$table->setPrimaryKey(['app', 'version']);
$this->connection->migrateToSchema($schema->getWrappedSchema());
$this->migrationTableCreated = true;
return true;
}
/**
* Returns all versions which have already been applied
*
* @return string[]
* @codeCoverageIgnore - no need to test this
*/
public function getMigratedVersions() {
$this->createMigrationTable();
$qb = $this->connection->getQueryBuilder();
$qb->select('version')
->from('migrations')
->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp())))
->orderBy('version');
$result = $qb->executeQuery();
$rows = $result->fetchAll(\PDO::FETCH_COLUMN);
$result->closeCursor();
return $rows;
}
/**
* Returns all versions which are available in the migration folder
* @return list<string>
*/
public function getAvailableVersions(): array {
$this->ensureMigrationsAreLoaded();
return array_map('strval', array_keys($this->migrations));
}
/**
* @return array<string, string>
*/
protected function findMigrations(): array {
$directory = realpath($this->migrationsPath);
if ($directory === false || !file_exists($directory) || !is_dir($directory)) {
return [];
}
$iterator = new \RegexIterator(
new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::LEAVES_ONLY
),
'#^.+\\/Version[^\\/]{1,255}\\.php$#i',
\RegexIterator::GET_MATCH);
$files = array_keys(iterator_to_array($iterator));
uasort($files, function ($a, $b) {
preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($a), $matchA);
preg_match('/^Version(\d+)Date(\d+)\\.php$/', basename($b), $matchB);
if (!empty($matchA) && !empty($matchB)) {
if ($matchA[1] !== $matchB[1]) {
return ($matchA[1] < $matchB[1]) ? -1 : 1;
}
return ($matchA[2] < $matchB[2]) ? -1 : 1;
}
return (basename($a) < basename($b)) ? -1 : 1;
});
$migrations = [];
foreach ($files as $file) {
$className = basename($file, '.php');
$version = (string) substr($className, 7);
if ($version === '0') {
throw new \InvalidArgumentException(
"Cannot load a migrations with the name '$version' because it is a reserved number"
);
}
$migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className);
}
return $migrations;
}
/**
* @param string $to
* @return string[]
*/
private function getMigrationsToExecute($to) {
$knownMigrations = $this->getMigratedVersions();
$availableMigrations = $this->getAvailableVersions();
$toBeExecuted = [];
foreach ($availableMigrations as $v) {
if ($to !== 'latest' && $v > $to) {
continue;
}
if ($this->shallBeExecuted($v, $knownMigrations)) {
$toBeExecuted[] = $v;
}
}
return $toBeExecuted;
}
/**
* @param string $m
* @param string[] $knownMigrations
* @return bool
*/
private function shallBeExecuted($m, $knownMigrations) {
if (in_array($m, $knownMigrations)) {
return false;
}
return true;
}
/**
* @param string $version
*/
private function markAsExecuted($version) {
$this->connection->insertIfNotExist('*PREFIX*migrations', [
'app' => $this->appName,
'version' => $version
]);
}
/**
* Returns the name of the table which holds the already applied versions
*
* @return string
*/
public function getMigrationsTableName() {
return $this->connection->getPrefix() . 'migrations';
}
/**
* Returns the namespace of the version classes
*
* @return string
*/
public function getMigrationsNamespace() {
return $this->migrationsNamespace;
}
/**
* Returns the directory which holds the versions
*
* @return string
*/
public function getMigrationsDirectory() {
return $this->migrationsPath;
}
/**
* Return the explicit version for the aliases; current, next, prev, latest
*
* @return mixed|null|string
*/
public function getMigration(string $alias) {
switch ($alias) {
case 'current':
return $this->getCurrentVersion();
case 'next':
return $this->getRelativeVersion($this->getCurrentVersion(), 1);
case 'prev':
return $this->getRelativeVersion($this->getCurrentVersion(), -1);
case 'latest':
$this->ensureMigrationsAreLoaded();
$migrations = $this->getAvailableVersions();
return @end($migrations);
}
return '0';
}
private function getRelativeVersion(string $version, int $delta): ?string {
$this->ensureMigrationsAreLoaded();
$versions = $this->getAvailableVersions();
array_unshift($versions, '0');
/** @var int $offset */
$offset = array_search($version, $versions, true);
if ($offset === false || !isset($versions[$offset + $delta])) {
// Unknown version or delta out of bounds.
return null;
}
return (string)$versions[$offset + $delta];
}
private function getCurrentVersion(): string {
$m = $this->getMigratedVersions();
if (count($m) === 0) {
return '0';
}
$migrations = array_values($m);
return @end($migrations);
}
/**
* @throws \InvalidArgumentException
*/
private function getClass(string $version): string {
$this->ensureMigrationsAreLoaded();
if (isset($this->migrations[$version])) {
return $this->migrations[$version];
}
throw new \InvalidArgumentException("Version $version is unknown.");
}
/**
* Allows to set an IOutput implementation which is used for logging progress and messages
*/
public function setOutput(IOutput $output): void {
$this->output = $output;
}
/**
* Applies all not yet applied versions up to $to
* @throws \InvalidArgumentException
*/
public function migrate(string $to = 'latest', bool $schemaOnly = false): void {
if ($schemaOnly) {
$this->output->debug('Migrating schema only');
$this->migrateSchemaOnly($to);
return;
}
// read known migrations
$toBeExecuted = $this->getMigrationsToExecute($to);
foreach ($toBeExecuted as $version) {
try {
$this->executeStep($version, $schemaOnly);
} catch (\Exception $e) {
// The exception itself does not contain the name of the migration,
// so we wrap it here, to make debugging easier.
throw new \Exception('Database error when running migration ' . $version . ' for app ' . $this->getApp() . PHP_EOL. $e->getMessage(), 0, $e);
}
}
}
/**
* Applies all not yet applied versions up to $to
* @throws \InvalidArgumentException
*/
public function migrateSchemaOnly(string $to = 'latest'): void {
// read known migrations
$toBeExecuted = $this->getMigrationsToExecute($to);
if (empty($toBeExecuted)) {
return;
}
$toSchema = null;
foreach ($toBeExecuted as $version) {
$this->output->debug('- Reading ' . $version);
$instance = $this->createInstance($version);
$toSchema = $instance->changeSchema($this->output, function () use ($toSchema): ISchemaWrapper {
return $toSchema ?: new SchemaWrapper($this->connection);
}, ['tablePrefix' => $this->connection->getPrefix()]) ?: $toSchema;
}
if ($toSchema instanceof SchemaWrapper) {
$this->output->debug('- Checking target database schema');
$targetSchema = $toSchema->getWrappedSchema();
$this->ensureUniqueNamesConstraints($targetSchema, true);
if ($this->checkOracle) {
$beforeSchema = $this->connection->createSchema();
$this->ensureOracleConstraints($beforeSchema, $targetSchema, strlen($this->connection->getPrefix()));
}
$this->output->debug('- Migrate database schema');
$this->connection->migrateToSchema($targetSchema);
$toSchema->performDropTableCalls();
}
$this->output->debug('- Mark migrations as executed');
foreach ($toBeExecuted as $version) {
$this->markAsExecuted($version);
}
}
/**
* Get the human readable descriptions for the migration steps to run
*
* @param string $to
* @return string[] [$name => $description]
*/
public function describeMigrationStep($to = 'latest') {
$toBeExecuted = $this->getMigrationsToExecute($to);
$description = [];
foreach ($toBeExecuted as $version) {
$migration = $this->createInstance($version);
if ($migration->name()) {
$description[$migration->name()] = $migration->description();
}
}
return $description;
}
/**
* @param string $version
* @return IMigrationStep
* @throws \InvalidArgumentException
*/
protected function createInstance($version) {
$class = $this->getClass($version);
try {
$s = \OCP\Server::get($class);
if (!$s instanceof IMigrationStep) {
throw new \InvalidArgumentException('Not a valid migration');
}
} catch (QueryException $e) {
if (class_exists($class)) {
$s = new $class();
} else {
throw new \InvalidArgumentException("Migration step '$class' is unknown");
}
}
return $s;
}
/**
* Executes one explicit version
*
* @param string $version
* @param bool $schemaOnly
* @throws \InvalidArgumentException
*/
public function executeStep($version, $schemaOnly = false) {
$instance = $this->createInstance($version);
if (!$schemaOnly) {
$instance->preSchemaChange($this->output, function (): ISchemaWrapper {
return new SchemaWrapper($this->connection);
}, ['tablePrefix' => $this->connection->getPrefix()]);
}
$toSchema = $instance->changeSchema($this->output, function (): ISchemaWrapper {
return new SchemaWrapper($this->connection);
}, ['tablePrefix' => $this->connection->getPrefix()]);
if ($toSchema instanceof SchemaWrapper) {
$targetSchema = $toSchema->getWrappedSchema();
$this->ensureUniqueNamesConstraints($targetSchema, $schemaOnly);
if ($this->checkOracle) {
$sourceSchema = $this->connection->createSchema();
$this->ensureOracleConstraints($sourceSchema, $targetSchema, strlen($this->connection->getPrefix()));
}
$this->connection->migrateToSchema($targetSchema);
$toSchema->performDropTableCalls();
}
if (!$schemaOnly) {
$instance->postSchemaChange($this->output, function (): ISchemaWrapper {
return new SchemaWrapper($this->connection);
}, ['tablePrefix' => $this->connection->getPrefix()]);
}
$this->markAsExecuted($version);
}
/**
* Naming constraints:
* - Tables names must be 30 chars or shorter (27 + oc_ prefix)
* - Column names must be 30 chars or shorter
* - Index names must be 30 chars or shorter
* - Sequence names must be 30 chars or shorter
* - Primary key names must be set or the table name 23 chars or shorter
*
* Data constraints:
* - Tables need a primary key (Not specific to Oracle, but required for performant clustering support)
* - Columns with "NotNull" can not have empty string as default value
* - Columns with "NotNull" can not have number 0 as default value
* - Columns with type "bool" (which is in fact integer of length 1) can not be "NotNull" as it can not store 0/false
* - Columns with type "string" can not be longer than 4.000 characters, use "text" instead
*
* @see https://github.com/nextcloud/documentation/blob/master/developer_manual/basics/storage/database.rst
*
* @param Schema $sourceSchema
* @param Schema $targetSchema
* @param int $prefixLength
* @throws \Doctrine\DBAL\Exception
*/
public function ensureOracleConstraints(Schema $sourceSchema, Schema $targetSchema, int $prefixLength) {
$sequences = $targetSchema->getSequences();
foreach ($targetSchema->getTables() as $table) {
try {
$sourceTable = $sourceSchema->getTable($table->getName());
} catch (SchemaException $e) {
if (\strlen($table->getName()) - $prefixLength > 27) {
throw new \InvalidArgumentException('Table name "' . $table->getName() . '" is too long.');
}
$sourceTable = null;
}
foreach ($table->getColumns() as $thing) {
// If the table doesn't exist OR if the column doesn't exist in the table
if (!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName())) {
if (\strlen($thing->getName()) > 30) {
throw new \InvalidArgumentException('Column name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
}
if ($thing->getNotnull() && $thing->getDefault() === ''
&& $sourceTable instanceof Table && !$sourceTable->hasColumn($thing->getName())) {
throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is NotNull, but has empty string or null as default.');
}
if ($thing->getNotnull() && $thing->getType()->getName() === Types::BOOLEAN) {
throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is type Bool and also NotNull, so it can not store "false".');
}
$sourceColumn = null;
} else {
$sourceColumn = $sourceTable->getColumn($thing->getName());
}
// If the column was just created OR the length changed OR the type changed
// we will NOT detect invalid length if the column is not modified
if (($sourceColumn === null || $sourceColumn->getLength() !== $thing->getLength() || $sourceColumn->getType()->getName() !== Types::STRING)
&& $thing->getLength() > 4000 && $thing->getType()->getName() === Types::STRING) {
throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $thing->getName() . '" is type String, but exceeding the 4.000 length limit.');
}
}
foreach ($table->getIndexes() as $thing) {
if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName())) && \strlen($thing->getName()) > 30) {
throw new \InvalidArgumentException('Index name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
}
}
foreach ($table->getForeignKeys() as $thing) {
if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName())) && \strlen($thing->getName()) > 30) {
throw new \InvalidArgumentException('Foreign key name "' . $table->getName() . '"."' . $thing->getName() . '" is too long.');
}
}
$primaryKey = $table->getPrimaryKey();
if ($primaryKey instanceof Index && (!$sourceTable instanceof Table || !$sourceTable->hasPrimaryKey())) {
$indexName = strtolower($primaryKey->getName());
$isUsingDefaultName = $indexName === 'primary';
if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) {
$defaultName = $table->getName() . '_pkey';
$isUsingDefaultName = strtolower($defaultName) === $indexName;
if ($isUsingDefaultName) {
$sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
$sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
return $sequence->getName() !== $sequenceName;
});
}
} elseif ($this->connection->getDatabasePlatform() instanceof OraclePlatform) {
$defaultName = $table->getName() . '_seq';
$isUsingDefaultName = strtolower($defaultName) === $indexName;
}
if (!$isUsingDefaultName && \strlen($indexName) > 30) {
throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" is too long.');
}
if ($isUsingDefaultName && \strlen($table->getName()) - $prefixLength >= 23) {
throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" is too long.');
}
} elseif (!$primaryKey instanceof Index && !$sourceTable instanceof Table) {
/** @var LoggerInterface $logger */
$logger = \OC::$server->get(LoggerInterface::class);
$logger->error('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups. This will throw an exception and not be installable in a future version of Nextcloud.');
// throw new \InvalidArgumentException('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups.');
}
}
foreach ($sequences as $sequence) {
if (!$sourceSchema->hasSequence($sequence->getName()) && \strlen($sequence->getName()) > 30) {
throw new \InvalidArgumentException('Sequence name "' . $sequence->getName() . '" is too long.');
}
}
}
/**
* Ensure naming constraints
*
* Naming constraints:
* - Index, sequence and primary key names must be unique within a Postgres Schema
*
* Only on installation we want to break hard, so that all developers notice
* the bugs when installing the app on any database or CI, and can work on
* fixing their migrations before releasing a version incompatible with Postgres.
*
* In case of updates we might be running on production instances and the
* administrators being faced with the error would not know how to resolve it
* anyway. This can also happen with instances, that had the issue before the
* current update, so we don't want to make their life more complicated
* than needed.
*
* @param Schema $targetSchema
* @param bool $isInstalling
*/
public function ensureUniqueNamesConstraints(Schema $targetSchema, bool $isInstalling): void {
$constraintNames = [];
$sequences = $targetSchema->getSequences();
foreach ($targetSchema->getTables() as $table) {
foreach ($table->getIndexes() as $thing) {
$indexName = strtolower($thing->getName());
if ($indexName === 'primary' || $thing->isPrimary()) {
continue;
}
if (isset($constraintNames[$thing->getName()])) {
if ($isInstalling) {
throw new \InvalidArgumentException('Index name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$this->logErrorOrWarning('Index name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$constraintNames[$thing->getName()] = $table->getName();
}
foreach ($table->getForeignKeys() as $thing) {
if (isset($constraintNames[$thing->getName()])) {
if ($isInstalling) {
throw new \InvalidArgumentException('Foreign key name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$this->logErrorOrWarning('Foreign key name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$constraintNames[$thing->getName()] = $table->getName();
}
$primaryKey = $table->getPrimaryKey();
if ($primaryKey instanceof Index) {
$indexName = strtolower($primaryKey->getName());
if ($indexName === 'primary') {
continue;
}
if (isset($constraintNames[$indexName])) {
if ($isInstalling) {
throw new \InvalidArgumentException('Primary index name "' . $indexName . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$this->logErrorOrWarning('Primary index name "' . $indexName . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$constraintNames[$indexName] = $table->getName();
}
}
foreach ($sequences as $sequence) {
if (isset($constraintNames[$sequence->getName()])) {
if ($isInstalling) {
throw new \InvalidArgumentException('Sequence name "' . $sequence->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$this->logErrorOrWarning('Sequence name "' . $sequence->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$constraintNames[$sequence->getName()] = 'sequence';
}
}
protected function logErrorOrWarning(string $log): void {
if ($this->output instanceof SimpleOutput) {
$this->output->warning($log);
} else {
$this->logger->error($log);
}
}
private function ensureMigrationsAreLoaded() {
if (empty($this->migrations)) {
$this->migrations = $this->findMigrations();
}
}
}
+193
View File
@@ -0,0 +1,193 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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\Connection;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\AbstractAsset;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaDiff;
use Doctrine\DBAL\Types\StringType;
use Doctrine\DBAL\Types\Type;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use function preg_match;
class Migrator {
/** @var Connection */
protected $connection;
/** @var IConfig */
protected $config;
private ?IEventDispatcher $dispatcher;
/** @var bool */
private $noEmit = false;
public function __construct(Connection $connection,
IConfig $config,
?IEventDispatcher $dispatcher = null) {
$this->connection = $connection;
$this->config = $config;
$this->dispatcher = $dispatcher;
}
/**
* @throws Exception
*/
public function migrate(Schema $targetSchema) {
$this->noEmit = true;
$this->applySchema($targetSchema);
}
/**
* @return string
*/
public function generateChangeScript(Schema $targetSchema) {
$schemaDiff = $this->getDiff($targetSchema, $this->connection);
$script = '';
$sqls = $this->connection->getDatabasePlatform()->getAlterSchemaSQL($schemaDiff);
foreach ($sqls as $sql) {
$script .= $this->convertStatementToScript($sql);
}
return $script;
}
/**
* @throws Exception
*/
public function createSchema() {
$this->connection->getConfiguration()->setSchemaAssetsFilter(function ($asset) {
/** @var string|AbstractAsset $asset */
$filterExpression = $this->getFilterExpression();
if ($asset instanceof AbstractAsset) {
return preg_match($filterExpression, $asset->getName()) === 1;
}
return preg_match($filterExpression, $asset) === 1;
});
return $this->connection->createSchemaManager()->introspectSchema();
}
/**
* @return SchemaDiff
*/
protected function getDiff(Schema $targetSchema, Connection $connection) {
// Adjust STRING columns with a length higher than 4000 to TEXT (clob)
// for consistency between the supported databases and
// old vs. new installations.
foreach ($targetSchema->getTables() as $table) {
foreach ($table->getColumns() as $column) {
if ($column->getType() instanceof StringType) {
if ($column->getLength() > 4000) {
$column->setType(Type::getType('text'));
$column->setLength(null);
}
}
}
}
$this->connection->getConfiguration()->setSchemaAssetsFilter(function ($asset) {
/** @var string|AbstractAsset $asset */
$filterExpression = $this->getFilterExpression();
if ($asset instanceof AbstractAsset) {
return preg_match($filterExpression, $asset->getName()) === 1;
}
return preg_match($filterExpression, $asset) === 1;
});
$sourceSchema = $connection->createSchemaManager()->introspectSchema();
// remove tables we don't know about
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 = $connection->createSchemaManager()->createComparator();
return $comparator->compareSchemas($sourceSchema, $targetSchema);
}
/**
* @throws Exception
*/
protected function applySchema(Schema $targetSchema, Connection $connection = null) {
if (is_null($connection)) {
$connection = $this->connection;
}
$schemaDiff = $this->getDiff($targetSchema, $connection);
if (!$connection->getDatabasePlatform() instanceof MySQLPlatform) {
$connection->beginTransaction();
}
$sqls = $connection->getDatabasePlatform()->getAlterSchemaSQL($schemaDiff);
$step = 0;
foreach ($sqls as $sql) {
$this->emit($sql, $step++, count($sqls));
$connection->executeQuery($sql);
}
if (!$connection->getDatabasePlatform() instanceof MySQLPlatform) {
$connection->commit();
}
}
/**
* @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->getSystemValueString('dbtableprefix', 'oc_'), '/') . '/';
}
protected function emit(string $sql, int $step, int $max): void {
if ($this->noEmit) {
return;
}
if (is_null($this->dispatcher)) {
return;
}
$this->dispatcher->dispatchTyped(new MigratorExecuteSqlEvent($sql, $step, $max));
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Côme Chilliet <come.chilliet@nextcloud.com>
*
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @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\EventDispatcher\Event;
class MigratorExecuteSqlEvent extends Event {
private string $sql;
private int $current;
private int $max;
public function __construct(
string $sql,
int $current,
int $max
) {
$this->sql = $sql;
$this->current = $current;
$this->max = $max;
}
public function getSql(): string {
return $this->sql;
}
public function getCurrentStep(): int {
return $this->current;
}
public function getMaxStep(): int {
return $this->max;
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB;
class MissingColumnInformation {
private array $listOfMissingColumns = [];
public function addHintForMissingColumn(string $tableName, string $columnName): void {
$this->listOfMissingColumns[] = [
'tableName' => $tableName,
'columnName' => $columnName,
];
}
public function getListOfMissingColumns(): array {
return $this->listOfMissingColumns;
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Morris Jobke <hey@morrisjobke.de>
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB;
class MissingIndexInformation {
private array $listOfMissingIndices = [];
public function addHintForMissingIndex(string $tableName, string $indexName): void {
$this->listOfMissingIndices[] = [
'tableName' => $tableName,
'indexName' => $indexName
];
}
public function getListOfMissingIndices(): array {
return $this->listOfMissingIndices;
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Morris Jobke <hey@morrisjobke.de>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB;
class MissingPrimaryKeyInformation {
private array $listOfMissingPrimaryKeys = [];
public function addHintForMissingPrimaryKey(string $tableName): void {
$this->listOfMissingPrimaryKeys[] = [
'tableName' => $tableName,
];
}
public function getListOfMissingPrimaryKeys(): array {
return $this->listOfMissingPrimaryKeys;
}
}
@@ -0,0 +1,69 @@
<?php
/**
* @copyright Copyright (c) 2017, ownCloud GmbH
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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\IDBConnection;
/**
* Various MySQL specific helper functions.
*/
class MySqlTools {
/**
* @param IDBConnection $connection
* @return bool
*/
public function supports4ByteCharset(IDBConnection $connection) {
$variables = ['innodb_file_per_table' => 'ON'];
if (!$this->isMariaDBWithLargePrefix($connection)) {
$variables['innodb_file_format'] = 'Barracuda';
$variables['innodb_large_prefix'] = 'ON';
}
foreach ($variables as $var => $val) {
$result = $connection->executeQuery("SHOW VARIABLES LIKE '$var'");
$row = $result->fetch();
$result->closeCursor();
if ($row === false) {
return false;
}
if (strcasecmp($row['Value'], $val) !== 0) {
return false;
}
}
return true;
}
protected function isMariaDBWithLargePrefix(IDBConnection $connection) {
$result = $connection->executeQuery('SELECT VERSION()');
$row = strtolower($result->fetchColumn());
$result->closeCursor();
if ($row === false) {
return false;
}
return str_contains($row, 'maria') && version_compare($row, '10.3', '>=') ||
!str_contains($row, 'maria') && version_compare($row, '8.0', '>=');
}
}
@@ -0,0 +1,25 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin Appelman <robin@icewind.nl>
*
* @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 {
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types = 1);
/*
* This file is part of the Symfony package.
*
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
* @author Fabien Potencier <fabien@symfony.com>
*
* @license AGPL-3.0-or-later AND MIT
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB;
final class ObjectParameter {
private $object;
private $error;
private $stringable;
private $class;
/**
* @param object $object
*/
public function __construct($object, ?\Throwable $error) {
$this->object = $object;
$this->error = $error;
$this->stringable = \is_callable([$object, '__toString']);
$this->class = \get_class($object);
}
/**
* @return object
*/
public function getObject() {
return $this->object;
}
public function getError(): ?\Throwable {
return $this->error;
}
public function isStringable(): bool {
return $this->stringable;
}
public function getClass(): string {
return $this->class;
}
}
@@ -0,0 +1,108 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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
* @param array<string, string> $data
* @return array<string, string>
*/
private function quoteKeys(array $data) {
$return = [];
$c = $this->getDatabasePlatform()->getIdentifierQuoteCharacter();
foreach ($data as $key => $value) {
if ($key[0] !== $c) {
$return[$this->quoteIdentifier($key)] = $value;
} else {
$return[$key] = $value;
}
}
return $return;
}
/**
* {@inheritDoc}
*/
public function insert($table, array $data, array $types = []) {
if ($table[0] !== $this->getDatabasePlatform()->getIdentifierQuoteCharacter()) {
$table = $this->quoteIdentifier($table);
}
$data = $this->quoteKeys($data);
return parent::insert($table, $data, $types);
}
/**
* {@inheritDoc}
*/
public function update($table, array $data, array $criteria, array $types = []) {
if ($table[0] !== $this->getDatabasePlatform()->getIdentifierQuoteCharacter()) {
$table = $this->quoteIdentifier($table);
}
$data = $this->quoteKeys($data);
$criteria = $this->quoteKeys($criteria);
return parent::update($table, $data, $criteria, $types);
}
/**
* {@inheritDoc}
*/
public function delete($table, array $criteria, array $types = []) {
if ($table[0] !== $this->getDatabasePlatform()->getIdentifierQuoteCharacter()) {
$table = $this->quoteIdentifier($table);
}
$criteria = $this->quoteKeys($criteria);
return parent::delete($table, $criteria);
}
/**
* 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([$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([$table]);
}
}
@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023, Joas Schilling <coding@schilljs.com>
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Piotr Mrowczynski <mrow4a@yahoo.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Victor Dubiniuk <dubiniuk@owncloud.com>
*
* @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\Exception;
use Doctrine\DBAL\Schema\Schema;
class OracleMigrator extends Migrator {
/**
* @param Schema $targetSchema
* @param \Doctrine\DBAL\Connection $connection
* @return \Doctrine\DBAL\Schema\SchemaDiff
* @throws Exception
*/
protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection): \Doctrine\DBAL\Schema\SchemaDiff {
// oracle forces us to quote the identifiers
$quotedSchema = new Schema();
foreach ($targetSchema->getTables() as $table) {
$quotedTable = $quotedSchema->createTable(
$this->connection->quoteIdentifier($table->getName()),
);
foreach ($table->getColumns() as $column) {
$newColumn = $quotedTable->addColumn(
$this->connection->quoteIdentifier($column->getName()),
$column->getType()->getTypeRegistry()->lookupName($column->getType()),
);
$newColumn->setAutoincrement($column->getAutoincrement());
$newColumn->setColumnDefinition($column->getColumnDefinition());
$newColumn->setComment($column->getComment());
$newColumn->setDefault($column->getDefault());
$newColumn->setFixed($column->getFixed());
$newColumn->setLength($column->getLength());
$newColumn->setNotnull($column->getNotnull());
$newColumn->setPrecision($column->getPrecision());
$newColumn->setScale($column->getScale());
$newColumn->setUnsigned($column->getUnsigned());
$newColumn->setPlatformOptions($column->getPlatformOptions());
}
foreach ($table->getIndexes() as $index) {
if ($index->isPrimary()) {
$quotedTable->setPrimaryKey(
array_map(function ($columnName) {
return $this->connection->quoteIdentifier($columnName);
}, $index->getColumns()),
//TODO migrate existing uppercase indexes, then $this->connection->quoteIdentifier($index->getName()),
$index->getName(),
);
} elseif ($index->isUnique()) {
$quotedTable->addUniqueIndex(
array_map(function ($columnName) {
return $this->connection->quoteIdentifier($columnName);
}, $index->getColumns()),
//TODO migrate existing uppercase indexes, then $this->connection->quoteIdentifier($index->getName()),
$index->getName(),
$index->getOptions(),
);
} else {
$quotedTable->addIndex(
array_map(function ($columnName) {
return $this->connection->quoteIdentifier($columnName);
}, $index->getColumns()),
//TODO migrate existing uppercase indexes, then $this->connection->quoteIdentifier($index->getName()),
$index->getName(),
$index->getFlags(),
$index->getOptions(),
);
}
}
foreach ($table->getUniqueConstraints() as $constraint) {
$quotedTable->addUniqueConstraint(
array_map(function ($columnName) {
return $this->connection->quoteIdentifier($columnName);
}, $constraint->getColumns()),
$this->connection->quoteIdentifier($constraint->getName()),
$constraint->getFlags(),
$constraint->getOptions(),
);
}
foreach ($table->getForeignKeys() as $foreignKey) {
$quotedTable->addForeignKeyConstraint(
$this->connection->quoteIdentifier($foreignKey->getForeignTableName()),
array_map(function ($columnName) {
return $this->connection->quoteIdentifier($columnName);
}, $foreignKey->getLocalColumns()),
array_map(function ($columnName) {
return $this->connection->quoteIdentifier($columnName);
}, $foreignKey->getForeignColumns()),
$foreignKey->getOptions(),
$this->connection->quoteIdentifier($foreignKey->getName()),
);
}
foreach ($table->getOptions() as $option => $value) {
$quotedTable->addOption(
$option,
$value,
);
}
}
foreach ($targetSchema->getSequences() as $sequence) {
$quotedSchema->createSequence(
$sequence->getName(),
$sequence->getAllocationSize(),
$sequence->getInitialValue(),
);
}
return parent::getDiff($quotedSchema, $connection);
}
/**
* @param $statement
* @return string
*/
protected function convertStatementToScript($statement) {
if (str_ends_with($statement, ';')) {
return $statement . PHP_EOL . '/' . PHP_EOL;
}
$script = $statement . ';';
$script .= PHP_EOL;
$script .= PHP_EOL;
return $script;
}
protected function getFilterExpression() {
return '/^"' . preg_quote($this->config->getSystemValueString('dbtableprefix', 'oc_')) . '/';
}
}
@@ -0,0 +1,83 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Andreas Fischer <bantu@owncloud.com>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author tbelau666 <thomas.belau@gmx.de>
*
* @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\AbstractAsset;
use OCP\IConfig;
use function preg_match;
use function preg_quote;
/**
* 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) {
$databaseName = $conn->getDatabase();
$conn->getConfiguration()->setSchemaAssetsFilter(function ($asset) {
/** @var string|AbstractAsset $asset */
$filterExpression = '/^' . preg_quote($this->config->getSystemValueString('dbtableprefix', 'oc_')) . '/';
if ($asset instanceof AbstractAsset) {
return preg_match($filterExpression, $asset->getName()) !== false;
}
return preg_match($filterExpression, $asset) !== false;
});
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 = ?';
$result = $conn->executeQuery($sqlInfo, [
"nextval('$sequenceName'::regclass)",
$databaseName
]);
$sequenceInfo = $result->fetchAssociative();
$result->free();
/** @var string $tableName */
$tableName = $sequenceInfo['table_name'];
/** @var string $columnName */
$columnName = $sequenceInfo['column_name'];
$sqlMaxId = "SELECT MAX($columnName) FROM $tableName";
$sqlSetval = "SELECT setval('$sequenceName', ($sqlMaxId))";
$conn->executeQuery($sqlSetval);
}
}
}
@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Statement;
use OCP\DB\IPreparedStatement;
use OCP\DB\IResult;
use PDO;
/**
* Adapts our public API to what doctrine/dbal exposed with 2.6
*
* The old dbal statement had stateful methods e.g. to fetch data from an executed
* prepared statement. To provide backwards compatibility to apps we need to make
* this class stateful. As soon as those now deprecated exposed methods are gone,
* we can limit the API of this adapter to the methods that map to the direct dbal
* methods without much magic.
*/
class PreparedStatement implements IPreparedStatement {
/** @var Statement */
private $statement;
/** @var IResult|null */
private $result;
public function __construct(Statement $statement) {
$this->statement = $statement;
}
public function closeCursor(): bool {
$this->getResult()->closeCursor();
return true;
}
public function fetch(int $fetchMode = PDO::FETCH_ASSOC) {
return $this->getResult()->fetch($fetchMode);
}
public function fetchAll(int $fetchMode = PDO::FETCH_ASSOC): array {
return $this->getResult()->fetchAll($fetchMode);
}
public function fetchColumn() {
return $this->getResult()->fetchOne();
}
public function fetchOne() {
return $this->getResult()->fetchOne();
}
public function bindValue($param, $value, $type = ParameterType::STRING): bool {
return $this->statement->bindValue($param, $value, $type);
}
public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool {
return $this->statement->bindParam($param, $variable, $type, $length);
}
public function execute($params = null): IResult {
return ($this->result = new ResultAdapter($this->statement->execute($params)));
}
public function rowCount(): int {
return $this->getResult()->rowCount();
}
private function getResult(): IResult {
if ($this->result !== null) {
return $this->result;
}
throw new Exception("You have to execute the prepared statement before accessing the results");
}
}
@@ -0,0 +1,92 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @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 = []): ICompositeExpression {
$this->compositeExpression->addMultiple($parts);
return $this;
}
/**
* Adds an expression to composite expression.
*
* @param mixed $part
*
* @return \OCP\DB\QueryBuilder\ICompositeExpression
*/
public function add($part): ICompositeExpression {
$this->compositeExpression->add($part);
return $this;
}
/**
* Retrieves the amount of expressions on composite expression.
*
* @return integer
*/
public function count(): int {
return $this->compositeExpression->count();
}
/**
* Returns the type of this composite expression (AND/OR).
*
* @return string
*/
public function getType(): string {
return $this->compositeExpression->getType();
}
/**
* Retrieves the string representation of this composite expression.
*
* @return string
*/
public function __toString(): string {
return (string) $this->compositeExpression;
}
}
@@ -0,0 +1,438 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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\ConnectionAdapter;
use OC\DB\QueryBuilder\CompositeExpression;
use OC\DB\QueryBuilder\FunctionBuilder\FunctionBuilder;
use OC\DB\QueryBuilder\Literal;
use OC\DB\QueryBuilder\QueryFunction;
use OC\DB\QueryBuilder\QuoteHelper;
use OCP\DB\QueryBuilder\ICompositeExpression;
use OCP\DB\QueryBuilder\IExpressionBuilder;
use OCP\DB\QueryBuilder\ILiteral;
use OCP\DB\QueryBuilder\IParameter;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\QueryBuilder\IQueryFunction;
use OCP\IDBConnection;
class ExpressionBuilder implements IExpressionBuilder {
/** @var \Doctrine\DBAL\Query\Expression\ExpressionBuilder */
protected $expressionBuilder;
/** @var QuoteHelper */
protected $helper;
/** @var IDBConnection */
protected $connection;
/** @var FunctionBuilder */
protected $functionBuilder;
/**
* Initializes a new <tt>ExpressionBuilder</tt>.
*
* @param ConnectionAdapter $connection
* @param IQueryBuilder $queryBuilder
*/
public function __construct(ConnectionAdapter $connection, IQueryBuilder $queryBuilder) {
$this->connection = $connection;
$this->helper = new QuoteHelper();
$this->expressionBuilder = new DoctrineExpressionBuilder($connection->getInner());
$this->functionBuilder = $queryBuilder->func();
}
/**
* 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): ICompositeExpression {
$compositeExpression = call_user_func_array([$this->expressionBuilder, 'andX'], $x);
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): ICompositeExpression {
$compositeExpression = call_user_func_array([$this->expressionBuilder, 'orX'], $x);
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, string $operator, $y, $type = null): string {
$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): string {
$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): string {
$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): string {
$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): string {
$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): string {
$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): string {
$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|ILiteral|IParameter|IQueryFunction $x The field in string format to be restricted by IS NULL.
*
* @return string
*/
public function isNull($x): string {
$x = $this->helper->quoteColumnName($x);
return $this->expressionBuilder->isNull($x);
}
/**
* Creates an IS NOT NULL expression with the given arguments.
*
* @param string|ILiteral|IParameter|IQueryFunction $x The field in string format to be restricted by IS NOT NULL.
*
* @return string
*/
public function isNotNull($x): string {
$x = $this->helper->quoteColumnName($x);
return $this->expressionBuilder->isNotNull($x);
}
/**
* Creates a LIKE() comparison expression with the given arguments.
*
* @param ILiteral|IParameter|IQueryFunction|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): string {
$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): string {
return $this->expressionBuilder->like($this->functionBuilder->lower($x), $this->functionBuilder->lower($y));
}
/**
* Creates a NOT LIKE() comparison expression with the given arguments.
*
* @param ILiteral|IParameter|IQueryFunction|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): string {
$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 ILiteral|IParameter|IQueryFunction|string $x The field in string format to be inspected by IN() comparison.
* @param ILiteral|IParameter|IQueryFunction|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): string {
$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 ILiteral|IParameter|IQueryFunction|string $x The field in string format to be inspected by NOT IN() comparison.
* @param ILiteral|IParameter|IQueryFunction|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): string {
$x = $this->helper->quoteColumnName($x);
$y = $this->helper->quoteColumnNames($y);
return $this->expressionBuilder->notIn($x, $y);
}
/**
* Creates a $x = '' statement, because Oracle needs a different check
*
* @param string|ILiteral|IParameter|IQueryFunction $x The field in string format to be inspected by the comparison.
* @return string
* @since 13.0.0
*/
public function emptyString($x): string {
return $this->eq($x, $this->literal('', IQueryBuilder::PARAM_STR));
}
/**
* Creates a `$x <> ''` statement, because Oracle needs a different check
*
* @param string|ILiteral|IParameter|IQueryFunction $x The field in string format to be inspected by the comparison.
* @return string
* @since 13.0.0
*/
public function nonEmptyString($x): string {
return $this->neq($x, $this->literal('', IQueryBuilder::PARAM_STR));
}
/**
* Binary AND Operator copies a bit to the result if it exists in both operands.
*
* @param string|ILiteral $x The field or value to check
* @param int $y Bitmap that must be set
* @return IQueryFunction
* @since 12.0.0
*/
public function bitwiseAnd($x, int $y): IQueryFunction {
return new QueryFunction($this->connection->getDatabasePlatform()->getBitAndComparisonExpression(
$this->helper->quoteColumnName($x),
$y
));
}
/**
* Binary OR Operator copies a bit if it exists in either operand.
*
* @param string|ILiteral $x The field or value to check
* @param int $y Bitmap that must be set
* @return IQueryFunction
* @since 12.0.0
*/
public function bitwiseOr($x, int $y): IQueryFunction {
return new QueryFunction($this->connection->getDatabasePlatform()->getBitOrComparisonExpression(
$this->helper->quoteColumnName($x),
$y
));
}
/**
* Quotes a given input parameter.
*
* @param mixed $input The parameter to be quoted.
* @param int $type One of the IQueryBuilder::PARAM_* constants
*
* @return ILiteral
*/
public function literal($input, $type = IQueryBuilder::PARAM_STR): ILiteral {
return new Literal($this->expressionBuilder->literal($input, $type));
}
/**
* Returns a IQueryFunction that casts the column to the given type
*
* @param string|IQueryFunction $column
* @param mixed $type One of IQueryBuilder::PARAM_*
* @psalm-param IQueryBuilder::PARAM_* $type
* @return IQueryFunction
*/
public function castColumn($column, $type): IQueryFunction {
return new QueryFunction(
$this->helper->quoteColumnName($column)
);
}
}
@@ -0,0 +1,73 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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\ConnectionAdapter;
use OC\DB\QueryBuilder\QueryFunction;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\QueryBuilder\IQueryFunction;
class MySqlExpressionBuilder extends ExpressionBuilder {
/** @var string */
protected $collation;
/**
* @param ConnectionAdapter $connection
* @param IQueryBuilder $queryBuilder
*/
public function __construct(ConnectionAdapter $connection, IQueryBuilder $queryBuilder) {
parent::__construct($connection, $queryBuilder);
$params = $connection->getInner()->getParams();
$this->collation = $params['collation'] ?? (($params['charset'] ?? 'utf8') . '_general_ci');
}
/**
* @inheritdoc
*/
public function iLike($x, $y, $type = null): string {
$x = $this->helper->quoteColumnName($x);
$y = $this->helper->quoteColumnName($y);
return $this->expressionBuilder->comparison($x, ' COLLATE ' . $this->collation . ' LIKE', $y);
}
/**
* Returns a IQueryFunction that casts the column to the given type
*
* @param string|IQueryFunction $column
* @param mixed $type One of IQueryBuilder::PARAM_*
* @psalm-param IQueryBuilder::PARAM_* $type
* @return IQueryFunction
*/
public function castColumn($column, $type): IQueryFunction {
switch ($type) {
case IQueryBuilder::PARAM_STR:
return new QueryFunction('CAST(' . $this->helper->quoteColumnName($column) . ' AS CHAR)');
default:
return parent::castColumn($column, $type);
}
}
}
@@ -0,0 +1,194 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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, string $operator, $y, $type = null): string {
$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): string {
$x = $this->prepareColumn($x, $type);
$y = $this->prepareColumn($y, $type);
return $this->expressionBuilder->eq($x, $y);
}
/**
* @inheritdoc
*/
public function neq($x, $y, $type = null): string {
$x = $this->prepareColumn($x, $type);
$y = $this->prepareColumn($y, $type);
return $this->expressionBuilder->neq($x, $y);
}
/**
* @inheritdoc
*/
public function lt($x, $y, $type = null): string {
$x = $this->prepareColumn($x, $type);
$y = $this->prepareColumn($y, $type);
return $this->expressionBuilder->lt($x, $y);
}
/**
* @inheritdoc
*/
public function lte($x, $y, $type = null): string {
$x = $this->prepareColumn($x, $type);
$y = $this->prepareColumn($y, $type);
return $this->expressionBuilder->lte($x, $y);
}
/**
* @inheritdoc
*/
public function gt($x, $y, $type = null): string {
$x = $this->prepareColumn($x, $type);
$y = $this->prepareColumn($y, $type);
return $this->expressionBuilder->gt($x, $y);
}
/**
* @inheritdoc
*/
public function gte($x, $y, $type = null): string {
$x = $this->prepareColumn($x, $type);
$y = $this->prepareColumn($y, $type);
return $this->expressionBuilder->gte($x, $y);
}
/**
* @inheritdoc
*/
public function in($x, $y, $type = null): string {
$x = $this->prepareColumn($x, $type);
$y = $this->prepareColumn($y, $type);
return $this->expressionBuilder->in($x, $y);
}
/**
* @inheritdoc
*/
public function notIn($x, $y, $type = null): string {
$x = $this->prepareColumn($x, $type);
$y = $this->prepareColumn($y, $type);
return $this->expressionBuilder->notIn($x, $y);
}
/**
* Creates a $x = '' statement, because Oracle needs a different check
*
* @param string|ILiteral|IParameter|IQueryFunction $x The field in string format to be inspected by the comparison.
* @return string
* @since 13.0.0
*/
public function emptyString($x): string {
return $this->isNull($x);
}
/**
* Creates a `$x <> ''` statement, because Oracle needs a different check
*
* @param string|ILiteral|IParameter|IQueryFunction $x The field in string format to be inspected by the comparison.
* @return string
* @since 13.0.0
*/
public function nonEmptyString($x): string {
return $this->isNotNull($x);
}
/**
* Returns a IQueryFunction that casts the column to the given type
*
* @param string|IQueryFunction $column
* @param mixed $type One of IQueryBuilder::PARAM_*
* @psalm-param IQueryBuilder::PARAM_* $type
* @return IQueryFunction
*/
public function castColumn($column, $type): IQueryFunction {
if ($type === IQueryBuilder::PARAM_STR) {
$column = $this->helper->quoteColumnName($column);
return new QueryFunction('to_char(' . $column . ')');
}
if ($type === IQueryBuilder::PARAM_INT) {
$column = $this->helper->quoteColumnName($column);
return new QueryFunction('to_number(to_char(' . $column . '))');
}
return parent::castColumn($column, $type);
}
/**
* @inheritdoc
*/
public function like($x, $y, $type = null): string {
return parent::like($x, $y, $type) . " ESCAPE '\\'";
}
/**
* @inheritdoc
*/
public function iLike($x, $y, $type = null): string {
return $this->like($this->functionBuilder->lower($x), $this->functionBuilder->lower($y));
}
}
@@ -0,0 +1,59 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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;
use OCP\DB\QueryBuilder\IQueryFunction;
class PgSqlExpressionBuilder extends ExpressionBuilder {
/**
* Returns a IQueryFunction that casts the column to the given type
*
* @param string|IQueryFunction $column
* @param mixed $type One of IQueryBuilder::PARAM_*
* @psalm-param IQueryBuilder::PARAM_* $type
* @return IQueryFunction
*/
public function castColumn($column, $type): IQueryFunction {
switch ($type) {
case IQueryBuilder::PARAM_INT:
return new QueryFunction('CAST(' . $this->helper->quoteColumnName($column) . ' AS INT)');
case IQueryBuilder::PARAM_STR:
return new QueryFunction('CAST(' . $this->helper->quoteColumnName($column) . ' AS TEXT)');
default:
return parent::castColumn($column, $type);
}
}
/**
* @inheritdoc
*/
public function iLike($x, $y, $type = null): string {
$x = $this->helper->quoteColumnName($x);
$y = $this->helper->quoteColumnName($y);
return $this->expressionBuilder->comparison($x, 'ILIKE', $y);
}
}
@@ -0,0 +1,37 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB\QueryBuilder\ExpressionBuilder;
class SqliteExpressionBuilder extends ExpressionBuilder {
/**
* @inheritdoc
*/
public function like($x, $y, $type = null): string {
return parent::like($x, $y, $type) . " ESCAPE '\\'";
}
public function iLike($x, $y, $type = null): string {
return $this->like($this->functionBuilder->lower($x), $this->functionBuilder->lower($y), $type);
}
}
@@ -0,0 +1,124 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB\QueryBuilder\FunctionBuilder;
use OC\DB\QueryBuilder\QueryFunction;
use OC\DB\QueryBuilder\QuoteHelper;
use OCP\DB\QueryBuilder\IFunctionBuilder;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\QueryBuilder\IQueryFunction;
use OCP\IDBConnection;
class FunctionBuilder implements IFunctionBuilder {
/** @var IDBConnection */
protected $connection;
/** @var IQueryBuilder */
protected $queryBuilder;
/** @var QuoteHelper */
protected $helper;
public function __construct(IDBConnection $connection, IQueryBuilder $queryBuilder, QuoteHelper $helper) {
$this->connection = $connection;
$this->queryBuilder = $queryBuilder;
$this->helper = $helper;
}
public function md5($input): IQueryFunction {
return new QueryFunction('MD5(' . $this->helper->quoteColumnName($input) . ')');
}
public function concat($x, ...$expr): IQueryFunction {
$args = func_get_args();
$list = [];
foreach ($args as $item) {
$list[] = $this->helper->quoteColumnName($item);
}
return new QueryFunction(sprintf('CONCAT(%s)', implode(', ', $list)));
}
public function groupConcat($expr, ?string $separator = ','): IQueryFunction {
$separator = $this->connection->quote($separator);
return new QueryFunction('GROUP_CONCAT(' . $this->helper->quoteColumnName($expr) . ' SEPARATOR ' . $separator . ')');
}
public function substring($input, $start, $length = null): IQueryFunction {
if ($length) {
return new QueryFunction('SUBSTR(' . $this->helper->quoteColumnName($input) . ', ' . $this->helper->quoteColumnName($start) . ', ' . $this->helper->quoteColumnName($length) . ')');
} else {
return new QueryFunction('SUBSTR(' . $this->helper->quoteColumnName($input) . ', ' . $this->helper->quoteColumnName($start) . ')');
}
}
public function sum($field): IQueryFunction {
return new QueryFunction('SUM(' . $this->helper->quoteColumnName($field) . ')');
}
public function lower($field): IQueryFunction {
return new QueryFunction('LOWER(' . $this->helper->quoteColumnName($field) . ')');
}
public function add($x, $y): IQueryFunction {
return new QueryFunction($this->helper->quoteColumnName($x) . ' + ' . $this->helper->quoteColumnName($y));
}
public function subtract($x, $y): IQueryFunction {
return new QueryFunction($this->helper->quoteColumnName($x) . ' - ' . $this->helper->quoteColumnName($y));
}
public function count($count = '', $alias = ''): IQueryFunction {
$alias = $alias ? (' AS ' . $this->helper->quoteColumnName($alias)) : '';
$quotedName = $count === '' ? '*' : $this->helper->quoteColumnName($count);
return new QueryFunction('COUNT(' . $quotedName . ')' . $alias);
}
public function octetLength($field, $alias = ''): IQueryFunction {
$alias = $alias ? (' AS ' . $this->helper->quoteColumnName($alias)) : '';
$quotedName = $this->helper->quoteColumnName($field);
return new QueryFunction('OCTET_LENGTH(' . $quotedName . ')' . $alias);
}
public function charLength($field, $alias = ''): IQueryFunction {
$alias = $alias ? (' AS ' . $this->helper->quoteColumnName($alias)) : '';
$quotedName = $this->helper->quoteColumnName($field);
return new QueryFunction('CHAR_LENGTH(' . $quotedName . ')' . $alias);
}
public function max($field): IQueryFunction {
return new QueryFunction('MAX(' . $this->helper->quoteColumnName($field) . ')');
}
public function min($field): IQueryFunction {
return new QueryFunction('MIN(' . $this->helper->quoteColumnName($field) . ')');
}
public function greatest($x, $y): IQueryFunction {
return new QueryFunction('GREATEST(' . $this->helper->quoteColumnName($x) . ', ' . $this->helper->quoteColumnName($y) . ')');
}
public function least($x, $y): IQueryFunction {
return new QueryFunction('LEAST(' . $this->helper->quoteColumnName($x) . ', ' . $this->helper->quoteColumnName($y) . ')');
}
}
@@ -0,0 +1,106 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB\QueryBuilder\FunctionBuilder;
use OC\DB\QueryBuilder\QueryFunction;
use OCP\DB\QueryBuilder\ILiteral;
use OCP\DB\QueryBuilder\IParameter;
use OCP\DB\QueryBuilder\IQueryFunction;
class OCIFunctionBuilder extends FunctionBuilder {
public function md5($input): IQueryFunction {
return new QueryFunction('LOWER(DBMS_OBFUSCATION_TOOLKIT.md5 (input => UTL_RAW.cast_to_raw(' . $this->helper->quoteColumnName($input) .')))');
}
/**
* As per https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions060.htm
* Oracle uses the first value to cast the rest or the values. So when the
* first value is a literal, plain value or column, instead of doing the
* math, it will cast the expression to int and continue with a "0". So when
* the second parameter is a function or column, we have to put that as
* first parameter.
*
* @param string|ILiteral|IParameter|IQueryFunction $x
* @param string|ILiteral|IParameter|IQueryFunction $y
* @return IQueryFunction
*/
public function greatest($x, $y): IQueryFunction {
if (is_string($y) || $y instanceof IQueryFunction) {
return parent::greatest($y, $x);
}
return parent::greatest($x, $y);
}
/**
* As per https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions060.htm
* Oracle uses the first value to cast the rest or the values. So when the
* first value is a literal, plain value or column, instead of doing the
* math, it will cast the expression to int and continue with a "0". So when
* the second parameter is a function or column, we have to put that as
* first parameter.
*
* @param string|ILiteral|IParameter|IQueryFunction $x
* @param string|ILiteral|IParameter|IQueryFunction $y
* @return IQueryFunction
*/
public function least($x, $y): IQueryFunction {
if (is_string($y) || $y instanceof IQueryFunction) {
return parent::least($y, $x);
}
return parent::least($x, $y);
}
public function concat($x, ...$expr): IQueryFunction {
$args = func_get_args();
$list = [];
foreach ($args as $item) {
$list[] = $this->helper->quoteColumnName($item);
}
return new QueryFunction(sprintf('(%s)', implode(' || ', $list)));
}
public function groupConcat($expr, ?string $separator = ','): IQueryFunction {
$orderByClause = ' WITHIN GROUP(ORDER BY NULL)';
if (is_null($separator)) {
return new QueryFunction('LISTAGG(' . $this->helper->quoteColumnName($expr) . ')' . $orderByClause);
}
$separator = $this->connection->quote($separator);
return new QueryFunction('LISTAGG(' . $this->helper->quoteColumnName($expr) . ', ' . $separator . ')' . $orderByClause);
}
public function octetLength($field, $alias = ''): IQueryFunction {
$alias = $alias ? (' AS ' . $this->helper->quoteColumnName($alias)) : '';
$quotedName = $this->helper->quoteColumnName($field);
return new QueryFunction('LENGTHB(' . $quotedName . ')' . $alias);
}
public function charLength($field, $alias = ''): IQueryFunction {
$alias = $alias ? (' AS ' . $this->helper->quoteColumnName($alias)) : '';
$quotedName = $this->helper->quoteColumnName($field);
return new QueryFunction('LENGTH(' . $quotedName . ')' . $alias);
}
}
@@ -0,0 +1,50 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB\QueryBuilder\FunctionBuilder;
use OC\DB\QueryBuilder\QueryFunction;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\QueryBuilder\IQueryFunction;
class PgSqlFunctionBuilder extends FunctionBuilder {
public function concat($x, ...$expr): IQueryFunction {
$args = func_get_args();
$list = [];
foreach ($args as $item) {
$list[] = $this->queryBuilder->expr()->castColumn($item, IQueryBuilder::PARAM_STR);
}
return new QueryFunction(sprintf('(%s)', implode(' || ', $list)));
}
public function groupConcat($expr, ?string $separator = ','): IQueryFunction {
$castedExpression = $this->queryBuilder->expr()->castColumn($expr, IQueryBuilder::PARAM_STR);
if (is_null($separator)) {
return new QueryFunction('string_agg(' . $castedExpression . ')');
}
$separator = $this->connection->quote($separator);
return new QueryFunction('string_agg(' . $castedExpression . ', ' . $separator . ')');
}
}
@@ -0,0 +1,63 @@
<?php
/**
* @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB\QueryBuilder\FunctionBuilder;
use OC\DB\QueryBuilder\QueryFunction;
use OCP\DB\QueryBuilder\IQueryFunction;
class SqliteFunctionBuilder extends FunctionBuilder {
public function concat($x, ...$expr): IQueryFunction {
$args = func_get_args();
$list = [];
foreach ($args as $item) {
$list[] = $this->helper->quoteColumnName($item);
}
return new QueryFunction(sprintf('(%s)', implode(' || ', $list)));
}
public function groupConcat($expr, ?string $separator = ','): IQueryFunction {
$separator = $this->connection->quote($separator);
return new QueryFunction('GROUP_CONCAT(' . $this->helper->quoteColumnName($expr) . ', ' . $separator . ')');
}
public function greatest($x, $y): IQueryFunction {
return new QueryFunction('MAX(' . $this->helper->quoteColumnName($x) . ', ' . $this->helper->quoteColumnName($y) . ')');
}
public function least($x, $y): IQueryFunction {
return new QueryFunction('MIN(' . $this->helper->quoteColumnName($x) . ', ' . $this->helper->quoteColumnName($y) . ')');
}
public function octetLength($field, $alias = ''): IQueryFunction {
$alias = $alias ? (' AS ' . $this->helper->quoteColumnName($alias)) : '';
$quotedName = $this->helper->quoteColumnName($field);
return new QueryFunction('LENGTH(CAST(' . $quotedName . ' as BLOB))' . $alias);
}
public function charLength($field, $alias = ''): IQueryFunction {
$alias = $alias ? (' AS ' . $this->helper->quoteColumnName($alias)) : '';
$quotedName = $this->helper->quoteColumnName($field);
return new QueryFunction('LENGTH(' . $quotedName . ')' . $alias);
}
}
@@ -0,0 +1,41 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
*
* @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;
}
}
@@ -0,0 +1,40 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @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;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,40 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @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;
}
}
@@ -0,0 +1,81 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @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');
}
$string = str_replace(' AS ', ' as ', $string);
if (substr_count($string, ' as ')) {
return implode(' as ', array_map([$this, 'quoteColumnName'], explode(' as ', $string, 2)));
}
if (substr_count($string, '.')) {
[$alias, $columnName] = explode('.', $string, 2);
if ($columnName === '*') {
return '`' . $alias . '`.*';
}
return '`' . $alias . '`.`' . $columnName . '`';
}
return '`' . $string . '`';
}
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB;
use Doctrine\DBAL\Result;
use OCP\DB\IResult;
use PDO;
/**
* Adapts DBAL 2.6 API for DBAL 3.x for backwards compatibility of a leaked type
*/
class ResultAdapter implements IResult {
/** @var Result */
private $inner;
public function __construct(Result $inner) {
$this->inner = $inner;
}
public function closeCursor(): bool {
$this->inner->free();
return true;
}
public function fetch(int $fetchMode = PDO::FETCH_ASSOC) {
return $this->inner->fetch($fetchMode);
}
public function fetchAll(int $fetchMode = PDO::FETCH_ASSOC): array {
if ($fetchMode !== PDO::FETCH_ASSOC && $fetchMode !== PDO::FETCH_NUM && $fetchMode !== PDO::FETCH_COLUMN) {
throw new \Exception('Fetch mode needs to be assoc, num or column.');
}
return $this->inner->fetchAll($fetchMode);
}
public function fetchColumn($columnIndex = 0) {
return $this->inner->fetchOne();
}
public function fetchOne() {
return $this->inner->fetchOne();
}
public function rowCount(): int {
return $this->inner->rowCount();
}
}
@@ -0,0 +1,46 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Robin Appelman <robin@icewind.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Victor Dubiniuk <dubiniuk@owncloud.com>
*
* @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 SQLiteMigrator extends Migrator {
/**
* @param Schema $targetSchema
* @param \Doctrine\DBAL\Connection $connection
* @return \Doctrine\DBAL\Schema\SchemaDiff
*/
protected function getDiff(Schema $targetSchema, \Doctrine\DBAL\Connection $connection) {
foreach ($targetSchema->getTables() as $table) {
foreach ($table->getColumns() as $column) {
// column comments are not supported on SQLite
if ($column->getComment() !== null) {
$column->setComment(null);
}
}
}
return parent::getDiff($targetSchema, $connection);
}
}
@@ -0,0 +1,71 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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\Common\EventSubscriber;
use Doctrine\DBAL\Event\ConnectionEventArgs;
use Doctrine\DBAL\Events;
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);
/** @var \Doctrine\DBAL\Driver\PDO\Connection $connection */
$connection = $args->getConnection()->getWrappedConnection();
$pdo = $connection->getWrappedConnection();
$pdo->sqliteCreateFunction('md5', 'md5', 1);
}
public function getSubscribedEvents() {
return [Events::postConnect];
}
}
@@ -0,0 +1,144 @@
<?php
/**
* @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Schema;
use OCP\DB\ISchemaWrapper;
class SchemaWrapper implements ISchemaWrapper {
/** @var Connection */
protected $connection;
/** @var Schema */
protected $schema;
/** @var array */
protected $tablesToDelete = [];
public function __construct(Connection $connection) {
$this->connection = $connection;
$this->schema = $this->connection->createSchema();
}
public function getWrappedSchema() {
return $this->schema;
}
public function performDropTableCalls() {
foreach ($this->tablesToDelete as $tableName => $true) {
$this->connection->dropTable($tableName);
unset($this->tablesToDelete[$tableName]);
}
}
/**
* Gets all table names
*
* @return array
*/
public function getTableNamesWithoutPrefix() {
$tableNames = $this->schema->getTableNames();
return array_map(function ($tableName) {
if (str_starts_with($tableName, $this->connection->getPrefix())) {
return substr($tableName, strlen($this->connection->getPrefix()));
}
return $tableName;
}, $tableNames);
}
// Overwritten methods
/**
* @return array
*/
public function getTableNames() {
return $this->schema->getTableNames();
}
/**
* @param string $tableName
*
* @return \Doctrine\DBAL\Schema\Table
* @throws \Doctrine\DBAL\Schema\SchemaException
*/
public function getTable($tableName) {
return $this->schema->getTable($this->connection->getPrefix() . $tableName);
}
/**
* Does this schema have a table with the given name?
*
* @param string $tableName
*
* @return boolean
*/
public function hasTable($tableName) {
return $this->schema->hasTable($this->connection->getPrefix() . $tableName);
}
/**
* Creates a new table.
*
* @param string $tableName
* @return \Doctrine\DBAL\Schema\Table
*/
public function createTable($tableName) {
unset($this->tablesToDelete[$tableName]);
return $this->schema->createTable($this->connection->getPrefix() . $tableName);
}
/**
* Drops a table from the schema.
*
* @param string $tableName
* @return \Doctrine\DBAL\Schema\Schema
*/
public function dropTable($tableName) {
$this->tablesToDelete[$tableName] = true;
return $this->schema->dropTable($this->connection->getPrefix() . $tableName);
}
/**
* Gets all tables of this schema.
*
* @return \Doctrine\DBAL\Schema\Table[]
*/
public function getTables() {
return $this->schema->getTables();
}
/**
* Gets the DatabasePlatform for the database.
*
* @return AbstractPlatform
*
* @throws Exception
*/
public function getDatabasePlatform() {
return $this->connection->getDatabasePlatform();
}
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019 Daniel Kesselberg <mail@danielkesselberg.de>
*
* @author Daniel Kesselberg <mail@danielkesselberg.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\DB;
use Doctrine\Common\EventSubscriber;
use Doctrine\DBAL\Event\ConnectionEventArgs;
use Doctrine\DBAL\Events;
use Doctrine\DBAL\TransactionIsolationLevel;
class SetTransactionIsolationLevel implements EventSubscriber {
/**
* @param ConnectionEventArgs $args
* @return void
*/
public function postConnect(ConnectionEventArgs $args) {
$args->getConnection()->setTransactionIsolation(TransactionIsolationLevel::READ_COMMITTED);
}
public function getSubscribedEvents() {
return [Events::postConnect];
}
}