You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

420 lines
12 KiB

10 years ago
10 years ago
10 years ago
10 years ago
13 years ago
13 years ago
13 years ago
13 years ago
11 years ago
13 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Bart Visscher <bartv@thisnet.nl>
  6. * @author Joas Schilling <coding@schilljs.com>
  7. * @author Morris Jobke <hey@morrisjobke.de>
  8. * @author Robin Appelman <robin@icewind.nl>
  9. * @author Robin McCorkell <robin@mccorkell.me.uk>
  10. * @author Roeland Jago Douma <roeland@famdouma.nl>
  11. * @author Thomas Müller <thomas.mueller@tmit.eu>
  12. *
  13. * @license AGPL-3.0
  14. *
  15. * This code is free software: you can redistribute it and/or modify
  16. * it under the terms of the GNU Affero General Public License, version 3,
  17. * as published by the Free Software Foundation.
  18. *
  19. * This program is distributed in the hope that it will be useful,
  20. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  21. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  22. * GNU Affero General Public License for more details.
  23. *
  24. * You should have received a copy of the GNU Affero General Public License, version 3,
  25. * along with this program. If not, see <http://www.gnu.org/licenses/>
  26. *
  27. */
  28. namespace OC\DB;
  29. use Doctrine\DBAL\DBALException;
  30. use Doctrine\DBAL\Driver;
  31. use Doctrine\DBAL\Configuration;
  32. use Doctrine\DBAL\Cache\QueryCacheProfile;
  33. use Doctrine\Common\EventManager;
  34. use Doctrine\DBAL\Platforms\MySqlPlatform;
  35. use OC\DB\QueryBuilder\QueryBuilder;
  36. use OCP\DB\QueryBuilder\IQueryBuilder;
  37. use OCP\IDBConnection;
  38. use OCP\PreConditionNotMetException;
  39. class Connection extends \Doctrine\DBAL\Connection implements IDBConnection {
  40. /**
  41. * @var string $tablePrefix
  42. */
  43. protected $tablePrefix;
  44. /**
  45. * @var \OC\DB\Adapter $adapter
  46. */
  47. protected $adapter;
  48. protected $lockedTable = null;
  49. public function connect() {
  50. try {
  51. return parent::connect();
  52. } catch (DBALException $e) {
  53. // throw a new exception to prevent leaking info from the stacktrace
  54. throw new DBALException('Failed to connect to the database: ' . $e->getMessage(), $e->getCode());
  55. }
  56. }
  57. /**
  58. * Returns a QueryBuilder for the connection.
  59. *
  60. * @return \OCP\DB\QueryBuilder\IQueryBuilder
  61. */
  62. public function getQueryBuilder() {
  63. return new QueryBuilder(
  64. $this,
  65. \OC::$server->getSystemConfig(),
  66. \OC::$server->getLogger()
  67. );
  68. }
  69. /**
  70. * Gets the QueryBuilder for the connection.
  71. *
  72. * @return \Doctrine\DBAL\Query\QueryBuilder
  73. * @deprecated please use $this->getQueryBuilder() instead
  74. */
  75. public function createQueryBuilder() {
  76. $backtrace = $this->getCallerBacktrace();
  77. \OC::$server->getLogger()->debug('Doctrine QueryBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
  78. return parent::createQueryBuilder();
  79. }
  80. /**
  81. * Gets the ExpressionBuilder for the connection.
  82. *
  83. * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder
  84. * @deprecated please use $this->getQueryBuilder()->expr() instead
  85. */
  86. public function getExpressionBuilder() {
  87. $backtrace = $this->getCallerBacktrace();
  88. \OC::$server->getLogger()->debug('Doctrine ExpressionBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
  89. return parent::getExpressionBuilder();
  90. }
  91. /**
  92. * Get the file and line that called the method where `getCallerBacktrace()` was used
  93. *
  94. * @return string
  95. */
  96. protected function getCallerBacktrace() {
  97. $traces = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
  98. // 0 is the method where we use `getCallerBacktrace`
  99. // 1 is the target method which uses the method we want to log
  100. if (isset($traces[1])) {
  101. return $traces[1]['file'] . ':' . $traces[1]['line'];
  102. }
  103. return '';
  104. }
  105. /**
  106. * @return string
  107. */
  108. public function getPrefix() {
  109. return $this->tablePrefix;
  110. }
  111. /**
  112. * Initializes a new instance of the Connection class.
  113. *
  114. * @param array $params The connection parameters.
  115. * @param \Doctrine\DBAL\Driver $driver
  116. * @param \Doctrine\DBAL\Configuration $config
  117. * @param \Doctrine\Common\EventManager $eventManager
  118. * @throws \Exception
  119. */
  120. public function __construct(array $params, Driver $driver, Configuration $config = null,
  121. EventManager $eventManager = null)
  122. {
  123. if (!isset($params['adapter'])) {
  124. throw new \Exception('adapter not set');
  125. }
  126. if (!isset($params['tablePrefix'])) {
  127. throw new \Exception('tablePrefix not set');
  128. }
  129. parent::__construct($params, $driver, $config, $eventManager);
  130. $this->adapter = new $params['adapter']($this);
  131. $this->tablePrefix = $params['tablePrefix'];
  132. parent::setTransactionIsolation(parent::TRANSACTION_READ_COMMITTED);
  133. }
  134. /**
  135. * Prepares an SQL statement.
  136. *
  137. * @param string $statement The SQL statement to prepare.
  138. * @param int $limit
  139. * @param int $offset
  140. * @return \Doctrine\DBAL\Driver\Statement The prepared statement.
  141. */
  142. public function prepare( $statement, $limit=null, $offset=null ) {
  143. if ($limit === -1) {
  144. $limit = null;
  145. }
  146. if (!is_null($limit)) {
  147. $platform = $this->getDatabasePlatform();
  148. $statement = $platform->modifyLimitQuery($statement, $limit, $offset);
  149. }
  150. $statement = $this->replaceTablePrefix($statement);
  151. $statement = $this->adapter->fixupStatement($statement);
  152. if(\OC::$server->getSystemConfig()->getValue( 'log_query', false)) {
  153. \OCP\Util::writeLog('core', 'DB prepare : '.$statement, \OCP\Util::DEBUG);
  154. }
  155. return parent::prepare($statement);
  156. }
  157. /**
  158. * Executes an, optionally parametrized, SQL query.
  159. *
  160. * If the query is parametrized, a prepared statement is used.
  161. * If an SQLLogger is configured, the execution is logged.
  162. *
  163. * @param string $query The SQL query to execute.
  164. * @param array $params The parameters to bind to the query, if any.
  165. * @param array $types The types the previous parameters are in.
  166. * @param \Doctrine\DBAL\Cache\QueryCacheProfile|null $qcp The query cache profile, optional.
  167. *
  168. * @return \Doctrine\DBAL\Driver\Statement The executed statement.
  169. *
  170. * @throws \Doctrine\DBAL\DBALException
  171. */
  172. public function executeQuery($query, array $params = array(), $types = array(), QueryCacheProfile $qcp = null)
  173. {
  174. $query = $this->replaceTablePrefix($query);
  175. $query = $this->adapter->fixupStatement($query);
  176. return parent::executeQuery($query, $params, $types, $qcp);
  177. }
  178. /**
  179. * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters
  180. * and returns the number of affected rows.
  181. *
  182. * This method supports PDO binding types as well as DBAL mapping types.
  183. *
  184. * @param string $query The SQL query.
  185. * @param array $params The query parameters.
  186. * @param array $types The parameter types.
  187. *
  188. * @return integer The number of affected rows.
  189. *
  190. * @throws \Doctrine\DBAL\DBALException
  191. */
  192. public function executeUpdate($query, array $params = array(), array $types = array())
  193. {
  194. $query = $this->replaceTablePrefix($query);
  195. $query = $this->adapter->fixupStatement($query);
  196. return parent::executeUpdate($query, $params, $types);
  197. }
  198. /**
  199. * Returns the ID of the last inserted row, or the last value from a sequence object,
  200. * depending on the underlying driver.
  201. *
  202. * Note: This method may not return a meaningful or consistent result across different drivers,
  203. * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY
  204. * columns or sequences.
  205. *
  206. * @param string $seqName Name of the sequence object from which the ID should be returned.
  207. * @return string A string representation of the last inserted ID.
  208. */
  209. public function lastInsertId($seqName = null) {
  210. if ($seqName) {
  211. $seqName = $this->replaceTablePrefix($seqName);
  212. }
  213. return $this->adapter->lastInsertId($seqName);
  214. }
  215. // internal use
  216. public function realLastInsertId($seqName = null) {
  217. return parent::lastInsertId($seqName);
  218. }
  219. /**
  220. * Insert a row if the matching row does not exists.
  221. *
  222. * @param string $table The table name (will replace *PREFIX* with the actual prefix)
  223. * @param array $input data that should be inserted into the table (column name => value)
  224. * @param array|null $compare List of values that should be checked for "if not exists"
  225. * If this is null or an empty array, all keys of $input will be compared
  226. * Please note: text fields (clob) must not be used in the compare array
  227. * @return int number of inserted rows
  228. * @throws \Doctrine\DBAL\DBALException
  229. */
  230. public function insertIfNotExist($table, $input, array $compare = null) {
  231. return $this->adapter->insertIfNotExist($table, $input, $compare);
  232. }
  233. private function getType($value) {
  234. if (is_bool($value)) {
  235. return IQueryBuilder::PARAM_BOOL;
  236. } else if (is_int($value)) {
  237. return IQueryBuilder::PARAM_INT;
  238. } else {
  239. return IQueryBuilder::PARAM_STR;
  240. }
  241. }
  242. /**
  243. * Insert or update a row value
  244. *
  245. * @param string $table
  246. * @param array $keys (column name => value)
  247. * @param array $values (column name => value)
  248. * @param array $updatePreconditionValues ensure values match preconditions (column name => value)
  249. * @return int number of new rows
  250. * @throws \Doctrine\DBAL\DBALException
  251. * @throws PreConditionNotMetException
  252. */
  253. public function setValues($table, array $keys, array $values, array $updatePreconditionValues = []) {
  254. try {
  255. $insertQb = $this->getQueryBuilder();
  256. $insertQb->insert($table)
  257. ->values(
  258. array_map(function($value) use ($insertQb) {
  259. return $insertQb->createNamedParameter($value, $this->getType($value));
  260. }, array_merge($keys, $values))
  261. );
  262. return $insertQb->execute();
  263. } catch (\Doctrine\DBAL\Exception\ConstraintViolationException $e) {
  264. // value already exists, try update
  265. $updateQb = $this->getQueryBuilder();
  266. $updateQb->update($table);
  267. foreach ($values as $name => $value) {
  268. $updateQb->set($name, $updateQb->createNamedParameter($value, $this->getType($value)));
  269. }
  270. $where = $updateQb->expr()->andX();
  271. $whereValues = array_merge($keys, $updatePreconditionValues);
  272. foreach ($whereValues as $name => $value) {
  273. $where->add($updateQb->expr()->eq(
  274. $name,
  275. $updateQb->createNamedParameter($value, $this->getType($value)),
  276. $this->getType($value)
  277. ));
  278. }
  279. $updateQb->where($where);
  280. $affected = $updateQb->execute();
  281. if ($affected === 0 && !empty($updatePreconditionValues)) {
  282. throw new PreConditionNotMetException();
  283. }
  284. return 0;
  285. }
  286. }
  287. /**
  288. * Create an exclusive read+write lock on a table
  289. *
  290. * @param string $tableName
  291. * @throws \BadMethodCallException When trying to acquire a second lock
  292. * @since 9.1.0
  293. */
  294. public function lockTable($tableName) {
  295. if ($this->lockedTable !== null) {
  296. throw new \BadMethodCallException('Can not lock a new table until the previous lock is released.');
  297. }
  298. $tableName = $this->tablePrefix . $tableName;
  299. $this->lockedTable = $tableName;
  300. $this->adapter->lockTable($tableName);
  301. }
  302. /**
  303. * Release a previous acquired lock again
  304. *
  305. * @since 9.1.0
  306. */
  307. public function unlockTable() {
  308. $this->adapter->unlockTable();
  309. $this->lockedTable = null;
  310. }
  311. /**
  312. * returns the error code and message as a string for logging
  313. * works with DoctrineException
  314. * @return string
  315. */
  316. public function getError() {
  317. $msg = $this->errorCode() . ': ';
  318. $errorInfo = $this->errorInfo();
  319. if (is_array($errorInfo)) {
  320. $msg .= 'SQLSTATE = '.$errorInfo[0] . ', ';
  321. $msg .= 'Driver Code = '.$errorInfo[1] . ', ';
  322. $msg .= 'Driver Message = '.$errorInfo[2];
  323. }
  324. return $msg;
  325. }
  326. /**
  327. * Drop a table from the database if it exists
  328. *
  329. * @param string $table table name without the prefix
  330. */
  331. public function dropTable($table) {
  332. $table = $this->tablePrefix . trim($table);
  333. $schema = $this->getSchemaManager();
  334. if($schema->tablesExist(array($table))) {
  335. $schema->dropTable($table);
  336. }
  337. }
  338. /**
  339. * Check if a table exists
  340. *
  341. * @param string $table table name without the prefix
  342. * @return bool
  343. */
  344. public function tableExists($table){
  345. $table = $this->tablePrefix . trim($table);
  346. $schema = $this->getSchemaManager();
  347. return $schema->tablesExist(array($table));
  348. }
  349. // internal use
  350. /**
  351. * @param string $statement
  352. * @return string
  353. */
  354. protected function replaceTablePrefix($statement) {
  355. return str_replace( '*PREFIX*', $this->tablePrefix, $statement );
  356. }
  357. /**
  358. * Check if a transaction is active
  359. *
  360. * @return bool
  361. * @since 8.2.0
  362. */
  363. public function inTransaction() {
  364. return $this->getTransactionNestingLevel() > 0;
  365. }
  366. /**
  367. * Espace a parameter to be used in a LIKE query
  368. *
  369. * @param string $param
  370. * @return string
  371. */
  372. public function escapeLikeParameter($param) {
  373. return addcslashes($param, '\\_%');
  374. }
  375. /**
  376. * Check whether or not the current database support 4byte wide unicode
  377. *
  378. * @return bool
  379. * @since 11.0.0
  380. */
  381. public function supports4ByteText() {
  382. return ! ($this->getDatabasePlatform() instanceof MySqlPlatform && $this->getParams()['charset'] !== 'utf8mb4');
  383. }
  384. }