PageRenderTime 51ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/RedBeanPHP/QueryWriter/AQueryWriter.php

http://github.com/gabordemooij/redbean
PHP | 1661 lines | 879 code | 224 blank | 558 comment | 154 complexity | fec0558780f9b74356e2f85579b196ee MD5 | raw file
  1. <?php
  2. namespace RedBeanPHP\QueryWriter;
  3. use RedBeanPHP\Adapter\DBAdapter as DBAdapter;
  4. use RedBeanPHP\RedException as RedException;
  5. use RedBeanPHP\QueryWriter as QueryWriter;
  6. use RedBeanPHP\OODBBean as OODBBean;
  7. use RedBeanPHP\RedException\SQL as SQLException;
  8. /**
  9. * RedBeanPHP Abstract Query Writer.
  10. * Represents an abstract Database to RedBean
  11. * To write a driver for a different database for RedBean
  12. * Contains a number of functions all implementors can
  13. * inherit or override.
  14. *
  15. * @file RedBeanPHP/QueryWriter/AQueryWriter.php
  16. * @author Gabor de Mooij and the RedBeanPHP Community
  17. * @license BSD/GPLv2
  18. *
  19. * @copyright
  20. * (c) copyright G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community.
  21. * This source file is subject to the BSD/GPLv2 License that is bundled
  22. * with this source code in the file license.txt.
  23. */
  24. abstract class AQueryWriter
  25. {
  26. /**
  27. * Constant: Select Snippet 'FOR UPDATE'
  28. */
  29. const C_SELECT_SNIPPET_FOR_UPDATE = 'FOR UPDATE';
  30. const C_DATA_TYPE_ONLY_IF_NOT_EXISTS = 80;
  31. const C_DATA_TYPE_MANUAL = 99;
  32. /**
  33. * @var array
  34. */
  35. private static $sqlFilters = array();
  36. /**
  37. * @var boolean
  38. */
  39. private static $flagSQLFilterSafeMode = FALSE;
  40. /**
  41. * @var boolean
  42. */
  43. private static $flagNarrowFieldMode = TRUE;
  44. /**
  45. * @var boolean
  46. */
  47. protected static $flagUseJSONColumns = FALSE;
  48. /**
  49. * @var boolean
  50. */
  51. protected static $enableISNULLConditions = FALSE;
  52. /**
  53. * @var array
  54. */
  55. public static $renames = array();
  56. /**
  57. * @var DBAdapter
  58. */
  59. protected $adapter;
  60. /**
  61. * @var string
  62. */
  63. protected $defaultValue = 'NULL';
  64. /**
  65. * @var string
  66. */
  67. protected $quoteCharacter = '';
  68. /**
  69. * @var boolean
  70. */
  71. protected $flagUseCache = TRUE;
  72. /**
  73. * @var array
  74. */
  75. protected $cache = array();
  76. /**
  77. * @var integer
  78. */
  79. protected $maxCacheSizePerType = 20;
  80. /**
  81. * @var string
  82. */
  83. protected $sqlSelectSnippet = '';
  84. /**
  85. * @var array
  86. */
  87. public $typeno_sqltype = array();
  88. /**
  89. * @var bool
  90. */
  91. protected static $noNuke = false;
  92. /**
  93. * Sets a data definition template to change the data
  94. * creation statements per type.
  95. *
  96. * For instance to add ROW_FORMAT=DYNAMIC to all MySQL tables
  97. * upon creation:
  98. *
  99. * $sql = $writer->getDDLTemplate( 'createTable', '*' );
  100. * $writer->setDDLTemplate( 'createTable', '*', $sql . ' ROW_FORMAT=DYNAMIC ' );
  101. *
  102. * For property-specific templates set $beanType to:
  103. * account.username -- then the template will only be applied to SQL statements relating
  104. * to that column/property.
  105. *
  106. * @param string $type ( 'createTable' | 'widenColumn' | 'addColumn' )
  107. * @param string $beanType ( type of bean or '*' to apply to all types )
  108. * @param string $template SQL template, contains %s for slots
  109. *
  110. * @return void
  111. */
  112. public function setDDLTemplate( $type, $beanType, $template )
  113. {
  114. $this->DDLTemplates[ $type ][ $beanType ] = $template;
  115. }
  116. /**
  117. * Returns the specified data definition template.
  118. * If no template can be found for the specified type, the template for
  119. * '*' will be returned instead.
  120. *
  121. * @param string $type ( 'createTable' | 'widenColumn' | 'addColumn' )
  122. * @param string $beanType ( type of bean or '*' to apply to all types )
  123. * @param string $property specify if you're looking for a property-specific template
  124. *
  125. * @return string
  126. */
  127. public function getDDLTemplate( $type, $beanType = '*', $property = NULL )
  128. {
  129. $key = ( $property ) ? "{$beanType}.{$property}" : $beanType;
  130. if ( isset( $this->DDLTemplates[ $type ][ $key ] ) ) {
  131. return $this->DDLTemplates[ $type ][ $key ];
  132. }
  133. if ( isset( $this->DDLTemplates[ $type ][ $beanType ] ) ) {
  134. return $this->DDLTemplates[ $type ][ $beanType ];
  135. }
  136. return $this->DDLTemplates[ $type ][ '*' ];
  137. }
  138. /**
  139. * Toggles support for IS-NULL-conditions.
  140. * If IS-NULL-conditions are enabled condition arrays
  141. * for functions including findLike() are treated so that
  142. * 'field' => NULL will be interpreted as field IS NULL
  143. * instead of being skipped. Returns the previous
  144. * value of the flag.
  145. *
  146. * @param boolean $flag TRUE or FALSE
  147. *
  148. * @return boolean
  149. */
  150. public static function useISNULLConditions( $flag )
  151. {
  152. $old = self::$enableISNULLConditions;
  153. self::$enableISNULLConditions = $flag;
  154. return $old;
  155. }
  156. /**
  157. * Toggles support for automatic generation of JSON columns.
  158. * Using JSON columns means that strings containing JSON will
  159. * cause the column to be created (not modified) as a JSON column.
  160. * However it might also trigger exceptions if this means the DB attempts to
  161. * convert a non-json column to a JSON column. Returns the previous
  162. * value of the flag.
  163. *
  164. * @param boolean $flag TRUE or FALSE
  165. *
  166. * @return boolean
  167. */
  168. public static function useJSONColumns( $flag )
  169. {
  170. $old = self::$flagUseJSONColumns;
  171. self::$flagUseJSONColumns = $flag;
  172. return $old;
  173. }
  174. /**
  175. * Toggles support for nuke().
  176. * Can be used to turn off the nuke() feature for security reasons.
  177. * Returns the old flag value.
  178. *
  179. * @param boolean $flag TRUE or FALSE
  180. *
  181. * @return boolean
  182. */
  183. public static function forbidNuke( $flag ) {
  184. $old = self::$noNuke;
  185. self::$noNuke = (bool) $flag;
  186. return $old;
  187. }
  188. /**
  189. * Checks whether a number can be treated like an int.
  190. *
  191. * @param string $value string representation of a certain value
  192. *
  193. * @return boolean
  194. */
  195. public static function canBeTreatedAsInt( $value )
  196. {
  197. return (bool) ( strval( $value ) === strval( intval( $value ) ) );
  198. }
  199. /**
  200. * @see QueryWriter::getAssocTableFormat
  201. */
  202. public static function getAssocTableFormat( $types )
  203. {
  204. sort( $types );
  205. $assoc = implode( '_', $types );
  206. return ( isset( self::$renames[$assoc] ) ) ? self::$renames[$assoc] : $assoc;
  207. }
  208. /**
  209. * @see QueryWriter::renameAssociation
  210. */
  211. public static function renameAssociation( $from, $to = NULL )
  212. {
  213. if ( is_array( $from ) ) {
  214. foreach ( $from as $key => $value ) self::$renames[$key] = $value;
  215. return;
  216. }
  217. self::$renames[$from] = $to;
  218. }
  219. /**
  220. * Globally available service method for RedBeanPHP.
  221. * Converts a camel cased string to a snake cased string.
  222. *
  223. * @param string $camel camelCased string to converty to snake case
  224. *
  225. * @return string
  226. */
  227. public static function camelsSnake( $camel )
  228. {
  229. return strtolower( preg_replace( '/(?<=[a-z])([A-Z])|([A-Z])(?=[a-z])/', '_$1$2', $camel ) );
  230. }
  231. /**
  232. * Clears renames.
  233. *
  234. * @return void
  235. */
  236. public static function clearRenames()
  237. {
  238. self::$renames = array();
  239. }
  240. /**
  241. * Toggles 'Narrow Field Mode'.
  242. * In Narrow Field mode the queryRecord method will
  243. * narrow its selection field to
  244. *
  245. * SELECT {table}.*
  246. *
  247. * instead of
  248. *
  249. * SELECT *
  250. *
  251. * This is a better way of querying because it allows
  252. * more flexibility (for instance joins). However if you need
  253. * the wide selector for backward compatibility; use this method
  254. * to turn OFF Narrow Field Mode by passing FALSE.
  255. * Default is TRUE.
  256. *
  257. * @param boolean $narrowField TRUE = Narrow Field FALSE = Wide Field
  258. *
  259. * @return void
  260. */
  261. public static function setNarrowFieldMode( $narrowField )
  262. {
  263. self::$flagNarrowFieldMode = (boolean) $narrowField;
  264. }
  265. /**
  266. * Sets SQL filters.
  267. * This is a lowlevel method to set the SQL filter array.
  268. * The format of this array is:
  269. *
  270. * <code>
  271. * array(
  272. * '<MODE, i.e. 'r' for read, 'w' for write>' => array(
  273. * '<TABLE NAME>' => array(
  274. * '<COLUMN NAME>' => '<SQL>'
  275. * )
  276. * )
  277. * )
  278. * </code>
  279. *
  280. * Example:
  281. *
  282. * <code>
  283. * array(
  284. * QueryWriter::C_SQLFILTER_READ => array(
  285. * 'book' => array(
  286. * 'title' => ' LOWER(book.title) '
  287. * )
  288. * )
  289. * </code>
  290. *
  291. * Note that you can use constants instead of magical chars
  292. * as keys for the uppermost array.
  293. * This is a lowlevel method. For a more friendly method
  294. * please take a look at the facade: R::bindFunc().
  295. *
  296. * @param array list of filters to set
  297. *
  298. * @return void
  299. */
  300. public static function setSQLFilters( $sqlFilters, $safeMode = FALSE )
  301. {
  302. self::$flagSQLFilterSafeMode = (boolean) $safeMode;
  303. self::$sqlFilters = $sqlFilters;
  304. }
  305. /**
  306. * Returns current SQL Filters.
  307. * This method returns the raw SQL filter array.
  308. * This is a lowlevel method. For a more friendly method
  309. * please take a look at the facade: R::bindFunc().
  310. *
  311. * @return array
  312. */
  313. public static function getSQLFilters()
  314. {
  315. return self::$sqlFilters;
  316. }
  317. /**
  318. * Returns a cache key for the cache values passed.
  319. * This method returns a fingerprint string to be used as a key to store
  320. * data in the writer cache.
  321. *
  322. * @param array $keyValues key-value to generate key for
  323. *
  324. * @return string
  325. */
  326. private function getCacheKey( $keyValues )
  327. {
  328. return json_encode( $keyValues );
  329. }
  330. /**
  331. * Returns the values associated with the provided cache tag and key.
  332. *
  333. * @param string $cacheTag cache tag to use for lookup
  334. * @param string $key key to use for lookup
  335. *
  336. * @return mixed
  337. */
  338. private function getCached( $cacheTag, $key )
  339. {
  340. $sql = $this->adapter->getSQL();
  341. if ($this->updateCache()) {
  342. if ( isset( $this->cache[$cacheTag][$key] ) ) {
  343. return $this->cache[$cacheTag][$key];
  344. }
  345. }
  346. return NULL;
  347. }
  348. /**
  349. * Checks if the previous query had a keep-cache tag.
  350. * If so, the cache will persist, otherwise the cache will be flushed.
  351. *
  352. * Returns TRUE if the cache will remain and FALSE if a flush has
  353. * been performed.
  354. *
  355. * @return boolean
  356. */
  357. private function updateCache()
  358. {
  359. $sql = $this->adapter->getSQL();
  360. if ( strpos( $sql, '-- keep-cache' ) !== strlen( $sql ) - 13 ) {
  361. // If SQL has been taken place outside of this method then something else then
  362. // a select query might have happened! (or instruct to keep cache)
  363. $this->cache = array();
  364. return FALSE;
  365. }
  366. return TRUE;
  367. }
  368. /**
  369. * Stores data from the writer in the cache under a specific key and cache tag.
  370. * A cache tag is used to make sure the cache remains consistent. In most cases the cache tag
  371. * will be the bean type, this makes sure queries associated with a certain reference type will
  372. * never contain conflicting data.
  373. * Why not use the cache tag as a key? Well
  374. * we need to make sure the cache contents fits the key (and key is based on the cache values).
  375. * Otherwise it would be possible to store two different result sets under the same key (the cache tag).
  376. *
  377. * In previous versions you could only store one key-entry, I have changed this to
  378. * improve caching efficiency (issue #400).
  379. *
  380. * @param string $cacheTag cache tag (secondary key)
  381. * @param string $key key to store values under
  382. * @param array $values content to be stored
  383. *
  384. * @return void
  385. */
  386. private function putResultInCache( $cacheTag, $key, $values )
  387. {
  388. if ( isset( $this->cache[$cacheTag] ) ) {
  389. if ( count( $this->cache[$cacheTag] ) > $this->maxCacheSizePerType ) array_shift( $this->cache[$cacheTag] );
  390. } else {
  391. $this->cache[$cacheTag] = array();
  392. }
  393. $this->cache[$cacheTag][$key] = $values;
  394. }
  395. /**
  396. * Creates an SQL snippet from a list of conditions of format:
  397. *
  398. * <code>
  399. * array(
  400. * key => array(
  401. * value1, value2, value3 ....
  402. * )
  403. * )
  404. * </code>
  405. *
  406. * @param array $conditions list of conditions
  407. * @param array $bindings parameter bindings for SQL snippet
  408. * @param string $addSql additional SQL snippet to append to result
  409. *
  410. * @return string
  411. */
  412. private function makeSQLFromConditions( $conditions, &$bindings, $addSql = '' )
  413. {
  414. reset( $bindings );
  415. $firstKey = key( $bindings );
  416. $paramTypeIsNum = ( is_numeric( $firstKey ) );
  417. $counter = 0;
  418. $sqlConditions = array();
  419. foreach ( $conditions as $column => $values ) {
  420. if ( $values === NULL ) {
  421. if ( self::$enableISNULLConditions ) {
  422. $sqlConditions[] = $this->esc( $column ) . ' IS NULL';
  423. }
  424. continue;
  425. }
  426. if ( is_array( $values ) ) {
  427. if ( empty( $values ) ) continue;
  428. } else {
  429. $values = array( $values );
  430. }
  431. $checkOODB = reset( $values );
  432. if ( $checkOODB instanceof OODBBean && $checkOODB->getMeta( 'type' ) === $column && substr( $column, -3 ) != '_id' )
  433. $column = $column . '_id';
  434. $sql = $this->esc( $column );
  435. $sql .= ' IN ( ';
  436. if ( $paramTypeIsNum ) {
  437. $sql .= implode( ',', array_fill( 0, count( $values ), '?' ) ) . ' ) ';
  438. array_unshift($sqlConditions, $sql);
  439. foreach ( $values as $k => $v ) {
  440. if ( $v instanceof OODBBean ) {
  441. $v = $v->id;
  442. }
  443. $values[$k] = strval( $v );
  444. array_unshift( $bindings, $v );
  445. }
  446. } else {
  447. $slots = array();
  448. foreach( $values as $k => $v ) {
  449. if ( $v instanceof OODBBean ) {
  450. $v = $v->id;
  451. }
  452. $slot = ':slot'.$counter++;
  453. $slots[] = $slot;
  454. $bindings[$slot] = strval( $v );
  455. }
  456. $sql .= implode( ',', $slots ).' ) ';
  457. $sqlConditions[] = $sql;
  458. }
  459. }
  460. $sql = '';
  461. if ( !empty( $sqlConditions ) ) {
  462. $sql .= " WHERE ( " . implode( ' AND ', $sqlConditions ) . ") ";
  463. }
  464. $addSql = $this->glueSQLCondition( $addSql, !empty( $sqlConditions ) ? QueryWriter::C_GLUE_AND : NULL );
  465. if ( $addSql ) $sql .= $addSql;
  466. return $sql;
  467. }
  468. /**
  469. * Returns the table names and column names for a relational query.
  470. *
  471. * @param string $sourceType type of the source bean
  472. * @param string $destType type of the bean you want to obtain using the relation
  473. * @param boolean $noQuote TRUE if you want to omit quotes
  474. *
  475. * @return array
  476. */
  477. private function getRelationalTablesAndColumns( $sourceType, $destType, $noQuote = FALSE )
  478. {
  479. $linkTable = $this->esc( $this->getAssocTable( array( $sourceType, $destType ) ), $noQuote );
  480. $sourceCol = $this->esc( $sourceType . '_id', $noQuote );
  481. if ( $sourceType === $destType ) {
  482. $destCol = $this->esc( $destType . '2_id', $noQuote );
  483. } else {
  484. $destCol = $this->esc( $destType . '_id', $noQuote );
  485. }
  486. $sourceTable = $this->esc( $sourceType, $noQuote );
  487. $destTable = $this->esc( $destType, $noQuote );
  488. return array( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol );
  489. }
  490. /**
  491. * Determines whether a string can be considered JSON or not.
  492. * This is used by writers that support JSON columns. However
  493. * we dont want that code duplicated over all JSON supporting
  494. * Query Writers.
  495. *
  496. * @param string $value value to determine 'JSONness' of.
  497. *
  498. * @return boolean
  499. */
  500. protected function isJSON( $value )
  501. {
  502. return (
  503. is_string($value) &&
  504. is_array(json_decode($value, TRUE)) &&
  505. (json_last_error() == JSON_ERROR_NONE)
  506. );
  507. }
  508. /**
  509. * Given a type and a property name this method
  510. * returns the foreign key map section associated with this pair.
  511. *
  512. * @param string $type name of the type
  513. * @param string $property name of the property
  514. *
  515. * @return array|NULL
  516. */
  517. protected function getForeignKeyForTypeProperty( $type, $property )
  518. {
  519. $property = $this->esc( $property, TRUE );
  520. try {
  521. $map = $this->getKeyMapForType( $type );
  522. } catch ( SQLException $e ) {
  523. return NULL;
  524. }
  525. foreach( $map as $key ) {
  526. if ( $key['from'] === $property ) return $key;
  527. }
  528. return NULL;
  529. }
  530. /**
  531. * Returns the foreign key map (FKM) for a type.
  532. * A foreign key map describes the foreign keys in a table.
  533. * A FKM always has the same structure:
  534. *
  535. * <code>
  536. * array(
  537. * 'name' => <name of the foreign key>
  538. * 'from' => <name of the column on the source table>
  539. * 'table' => <name of the target table>
  540. * 'to' => <name of the target column> (most of the time 'id')
  541. * 'on_update' => <update rule: 'SET NULL','CASCADE' or 'RESTRICT'>
  542. * 'on_delete' => <delete rule: 'SET NULL','CASCADE' or 'RESTRICT'>
  543. * )
  544. * </code>
  545. *
  546. * @note the keys in the result array are FKDLs, i.e. descriptive unique
  547. * keys per source table. Also see: AQueryWriter::makeFKLabel for details.
  548. *
  549. * @param string $type the bean type you wish to obtain a key map of
  550. *
  551. * @return array
  552. */
  553. protected function getKeyMapForType( $type )
  554. {
  555. return array();
  556. }
  557. /**
  558. * This method makes a key for a foreign key description array.
  559. * This key is a readable string unique for every source table.
  560. * This uniform key is called the FKDL Foreign Key Description Label.
  561. * Note that the source table is not part of the FKDL because
  562. * this key is supposed to be 'per source table'. If you wish to
  563. * include a source table, prefix the key with 'on_table_<SOURCE>_'.
  564. *
  565. * @param string $from the column of the key in the source table
  566. * @param string $type the type (table) where the key points to
  567. * @param string $to the target column of the foreign key (mostly just 'id')
  568. *
  569. * @return string
  570. */
  571. protected function makeFKLabel($from, $type, $to)
  572. {
  573. return "from_{$from}_to_table_{$type}_col_{$to}";
  574. }
  575. /**
  576. * Returns an SQL Filter snippet for reading.
  577. *
  578. * @param string $type type of bean
  579. *
  580. * @return string
  581. */
  582. protected function getSQLFilterSnippet( $type )
  583. {
  584. $existingCols = array();
  585. if (self::$flagSQLFilterSafeMode) {
  586. $existingCols = $this->getColumns( $type );
  587. }
  588. $sqlFilters = array();
  589. if ( isset( self::$sqlFilters[QueryWriter::C_SQLFILTER_READ][$type] ) ) {
  590. foreach( self::$sqlFilters[QueryWriter::C_SQLFILTER_READ][$type] as $property => $sqlFilter ) {
  591. if ( !self::$flagSQLFilterSafeMode || isset( $existingCols[$property] ) ) {
  592. $sqlFilters[] = $sqlFilter.' AS '.$property.' ';
  593. }
  594. }
  595. }
  596. $sqlFilterStr = ( count($sqlFilters) ) ? ( ','.implode( ',', $sqlFilters ) ) : '';
  597. return $sqlFilterStr;
  598. }
  599. /**
  600. * Generates a list of parameters (slots) for an SQL snippet.
  601. * This method calculates the correct number of slots to insert in the
  602. * SQL snippet and determines the correct type of slot. If the bindings
  603. * array contains named parameters this method will return named ones and
  604. * update the keys in the value list accordingly (that's why we use the &).
  605. *
  606. * If you pass an offset the bindings will be re-added to the value list.
  607. * Some databases cant handle duplicate parameter names in queries.
  608. *
  609. * @param array &$valueList list of values to generate slots for (gets modified if needed)
  610. * @param array $otherBindings list of additional bindings
  611. * @param integer $offset start counter at...
  612. *
  613. * @return string
  614. */
  615. protected function getParametersForInClause( &$valueList, $otherBindings, $offset = 0 )
  616. {
  617. if ( is_array( $otherBindings ) && count( $otherBindings ) > 0 ) {
  618. reset( $otherBindings );
  619. $key = key( $otherBindings );
  620. if ( !is_numeric($key) ) {
  621. $filler = array();
  622. $newList = (!$offset) ? array() : $valueList;
  623. $counter = $offset;
  624. foreach( $valueList as $value ) {
  625. $slot = ':slot' . ( $counter++ );
  626. $filler[] = $slot;
  627. $newList[$slot] = $value;
  628. }
  629. // Change the keys!
  630. $valueList = $newList;
  631. return implode( ',', $filler );
  632. }
  633. }
  634. return implode( ',', array_fill( 0, count( $valueList ), '?' ) );
  635. }
  636. /**
  637. * Adds a data type to the list of data types.
  638. * Use this method to add a new column type definition to the writer.
  639. * Used for UUID support.
  640. *
  641. * @param integer $dataTypeID magic number constant assigned to this data type
  642. * @param string $SQLDefinition SQL column definition (i.e. INT(11))
  643. *
  644. * @return self
  645. */
  646. protected function addDataType( $dataTypeID, $SQLDefinition )
  647. {
  648. $this->typeno_sqltype[ $dataTypeID ] = $SQLDefinition;
  649. $this->sqltype_typeno[ $SQLDefinition ] = $dataTypeID;
  650. return $this;
  651. }
  652. /**
  653. * Returns the sql that should follow an insert statement.
  654. *
  655. * @param string $table name
  656. *
  657. * @return string
  658. */
  659. protected function getInsertSuffix( $table )
  660. {
  661. return '';
  662. }
  663. /**
  664. * Checks whether a value starts with zeros. In this case
  665. * the value should probably be stored using a text datatype instead of a
  666. * numerical type in order to preserve the zeros.
  667. *
  668. * @param string $value value to be checked.
  669. *
  670. * @return boolean
  671. */
  672. protected function startsWithZeros( $value )
  673. {
  674. $value = strval( $value );
  675. if ( strlen( $value ) > 1 && strpos( $value, '0' ) === 0 && strpos( $value, '0.' ) !== 0 ) {
  676. return TRUE;
  677. } else {
  678. return FALSE;
  679. }
  680. }
  681. /**
  682. * Inserts a record into the database using a series of insert columns
  683. * and corresponding insertvalues. Returns the insert id.
  684. *
  685. * @param string $table table to perform query on
  686. * @param array $insertcolumns columns to be inserted
  687. * @param array $insertvalues values to be inserted
  688. *
  689. * @return integer
  690. */
  691. protected function insertRecord( $type, $insertcolumns, $insertvalues )
  692. {
  693. $default = $this->defaultValue;
  694. $suffix = $this->getInsertSuffix( $type );
  695. $table = $this->esc( $type );
  696. if ( count( $insertvalues ) > 0 && is_array( $insertvalues[0] ) && count( $insertvalues[0] ) > 0 ) {
  697. $insertSlots = array();
  698. foreach ( $insertcolumns as $k => $v ) {
  699. $insertcolumns[$k] = $this->esc( $v );
  700. if (isset(self::$sqlFilters['w'][$type][$v])) {
  701. $insertSlots[] = self::$sqlFilters['w'][$type][$v];
  702. } else {
  703. $insertSlots[] = '?';
  704. }
  705. }
  706. $insertSQL = "INSERT INTO $table ( id, " . implode( ',', $insertcolumns ) . " ) VALUES
  707. ( $default, " . implode( ',', $insertSlots ) . " ) $suffix";
  708. $ids = array();
  709. foreach ( $insertvalues as $i => $insertvalue ) {
  710. $ids[] = $this->adapter->getCell( $insertSQL, $insertvalue, $i );
  711. }
  712. $result = count( $ids ) === 1 ? array_pop( $ids ) : $ids;
  713. } else {
  714. $result = $this->adapter->getCell( "INSERT INTO $table (id) VALUES($default) $suffix" );
  715. }
  716. if ( $suffix ) return $result;
  717. $last_id = $this->adapter->getInsertID();
  718. return $last_id;
  719. }
  720. /**
  721. * Checks table name or column name.
  722. *
  723. * @param string $table table string
  724. *
  725. * @return string
  726. */
  727. protected function check( $struct )
  728. {
  729. if ( !is_string( $struct ) || !preg_match( '/^[a-zA-Z0-9_]+$/', $struct ) ) {
  730. throw new RedException( 'Identifier does not conform to RedBeanPHP security policies.' );
  731. }
  732. return $struct;
  733. }
  734. /**
  735. * Checks whether the specified type (i.e. table) already exists in the database.
  736. * Not part of the Object Database interface!
  737. *
  738. * @param string $table table name
  739. *
  740. * @return boolean
  741. */
  742. public function tableExists( $table )
  743. {
  744. $tables = $this->getTables();
  745. return in_array( $table, $tables );
  746. }
  747. /**
  748. * @see QueryWriter::glueSQLCondition
  749. */
  750. public function glueSQLCondition( $sql, $glue = NULL )
  751. {
  752. static $snippetCache = array();
  753. if ( trim( $sql ) === '' ) {
  754. return $sql;
  755. }
  756. $key = $glue . '|' . $sql;
  757. if ( isset( $snippetCache[$key] ) ) {
  758. return $snippetCache[$key];
  759. }
  760. $lsql = ltrim( $sql );
  761. if ( preg_match( '/^(INNER|LEFT|RIGHT|JOIN|AND|OR|WHERE|ORDER|GROUP|HAVING|LIMIT|OFFSET)\s+/i', $lsql ) ) {
  762. if ( $glue === QueryWriter::C_GLUE_WHERE && stripos( $lsql, 'AND' ) === 0 ) {
  763. $snippetCache[$key] = ' WHERE ' . substr( $lsql, 3 );
  764. } else {
  765. $snippetCache[$key] = $sql;
  766. }
  767. } else {
  768. $snippetCache[$key] = ( ( $glue === QueryWriter::C_GLUE_AND ) ? ' AND ' : ' WHERE ') . $sql;
  769. }
  770. return $snippetCache[$key];
  771. }
  772. /**
  773. * @see QueryWriter::glueLimitOne
  774. */
  775. public function glueLimitOne( $sql = '')
  776. {
  777. return ( strpos( strtoupper( ' ' . $sql ), ' LIMIT ' ) === FALSE ) ? ( $sql . ' LIMIT 1 ' ) : $sql;
  778. }
  779. /**
  780. * @see QueryWriter::esc
  781. */
  782. public function esc( $dbStructure, $dontQuote = FALSE )
  783. {
  784. $this->check( $dbStructure );
  785. return ( $dontQuote ) ? $dbStructure : $this->quoteCharacter . $dbStructure . $this->quoteCharacter;
  786. }
  787. /**
  788. * @see QueryWriter::addColumn
  789. */
  790. public function addColumn( $beanType, $column, $field )
  791. {
  792. $table = $beanType;
  793. $type = $field;
  794. $table = $this->esc( $table );
  795. $column = $this->esc( $column );
  796. $type = ( isset( $this->typeno_sqltype[$type] ) ) ? $this->typeno_sqltype[$type] : '';
  797. $this->adapter->exec( sprintf( $this->getDDLTemplate('addColumn', $beanType, $column ), $table, $column, $type ) );
  798. }
  799. /**
  800. * @see QueryWriter::updateRecord
  801. */
  802. public function updateRecord( $type, $updatevalues, $id = NULL )
  803. {
  804. $table = $type;
  805. if ( !$id ) {
  806. $insertcolumns = $insertvalues = array();
  807. foreach ( $updatevalues as $pair ) {
  808. $insertcolumns[] = $pair['property'];
  809. $insertvalues[] = $pair['value'];
  810. }
  811. //Otherwise psql returns string while MySQL/SQLite return numeric causing problems with additions (array_diff)
  812. return (string) $this->insertRecord( $table, $insertcolumns, array( $insertvalues ) );
  813. }
  814. if ( $id && !count( $updatevalues ) ) {
  815. return $id;
  816. }
  817. $table = $this->esc( $table );
  818. $sql = "UPDATE $table SET ";
  819. $p = $v = array();
  820. foreach ( $updatevalues as $uv ) {
  821. if ( isset( self::$sqlFilters['w'][$type][$uv['property']] ) ) {
  822. $p[] = " {$this->esc( $uv["property"] )} = ". self::$sqlFilters['w'][$type][$uv['property']];
  823. } else {
  824. $p[] = " {$this->esc( $uv["property"] )} = ? ";
  825. }
  826. $v[] = $uv['value'];
  827. }
  828. $sql .= implode( ',', $p ) . ' WHERE id = ? ';
  829. $v[] = $id;
  830. $this->adapter->exec( $sql, $v );
  831. return $id;
  832. }
  833. /**
  834. * @see QueryWriter::parseJoin
  835. */
  836. public function parseJoin( $type, $sql, $cteType = NULL )
  837. {
  838. if ( strpos( $sql, '@' ) === FALSE ) {
  839. return $sql;
  840. }
  841. $sql = ' ' . $sql;
  842. $joins = array();
  843. $joinSql = '';
  844. if ( !preg_match_all( '#@((shared|own|joined)\.[^\s(,=!?]+)#', $sql, $matches ) )
  845. return $sql;
  846. $expressions = $matches[1];
  847. // Sort to make the joins from the longest to the shortest
  848. uasort( $expressions, function($a, $b) {
  849. return substr_count( $b, '.' ) - substr_count( $a, '.' );
  850. });
  851. $nsuffix = 1;
  852. foreach ( $expressions as $exp ) {
  853. $explosion = explode( '.', $exp );
  854. $joinTable = $type;
  855. $joinType = array_shift( $explosion );
  856. $lastPart = array_pop( $explosion );
  857. $lastJoin = end($explosion);
  858. if ( ( $index = strpos( $lastJoin, '[' ) ) !== FALSE ) {
  859. $lastJoin = substr( $lastJoin, 0, $index);
  860. }
  861. reset($explosion);
  862. // Let's check if we already joined that chain
  863. // If that's the case we skip this
  864. $joinKey = implode( '.', $explosion );
  865. foreach ( $joins as $chain => $suffix ) {
  866. if ( strpos ( $chain, $joinKey ) === 0 ) {
  867. $sql = str_replace( "@{$exp}", "{$lastJoin}__rb{$suffix}.{$lastPart}", $sql );
  868. continue 2;
  869. }
  870. }
  871. $sql = str_replace( "@{$exp}", "{$lastJoin}__rb{$nsuffix}.{$lastPart}", $sql );
  872. $joins[$joinKey] = $nsuffix;
  873. // We loop on the elements of the join
  874. $i = 0;
  875. while ( TRUE ) {
  876. $joinInfo = $explosion[$i];
  877. if ( $i ) {
  878. $joinType = $explosion[$i-1];
  879. $joinTable = $explosion[$i-2];
  880. }
  881. $aliases = array();
  882. if ( ( $index = strpos( $joinInfo, '[' ) ) !== FALSE ) {
  883. if ( preg_match_all( '#(([^\s:/\][]+)[/\]])#', $joinInfo, $matches ) ) {
  884. $aliases = $matches[2];
  885. $joinInfo = substr( $joinInfo, 0, $index);
  886. }
  887. }
  888. if ( ( $index = strpos( $joinTable, '[' ) ) !== FALSE ) {
  889. $joinTable = substr( $joinTable, 0, $index);
  890. }
  891. if ( $i ) {
  892. $joinSql .= $this->writeJoin( $joinTable, $joinInfo, 'INNER', $joinType, FALSE, "__rb{$nsuffix}", $aliases, NULL );
  893. } else {
  894. $joinSql .= $this->writeJoin( $joinTable, $joinInfo, 'LEFT', $joinType, TRUE, "__rb{$nsuffix}", $aliases, $cteType );
  895. }
  896. $i += 2;
  897. if ( !isset( $explosion[$i] ) ) {
  898. break;
  899. }
  900. }
  901. $nsuffix++;
  902. }
  903. $sql = str_ireplace( ' where ', ' WHERE ', $sql );
  904. if ( strpos( $sql, ' WHERE ') === FALSE ) {
  905. if ( preg_match( '/^(ORDER|GROUP|HAVING|LIMIT|OFFSET)\s+/i', trim($sql) ) ) {
  906. $sql = "{$joinSql} {$sql}";
  907. } else {
  908. $sql = "{$joinSql} WHERE {$sql}";
  909. }
  910. } else {
  911. $sqlParts = explode( ' WHERE ', $sql, 2 );
  912. $sql = "{$sqlParts[0]} {$joinSql} WHERE {$sqlParts[1]}";
  913. }
  914. return $sql;
  915. }
  916. /**
  917. * @see QueryWriter::writeJoin
  918. */
  919. public function writeJoin( $type, $targetType, $leftRight = 'LEFT', $joinType = 'parent', $firstOfChain = TRUE, $suffix = '', $aliases = array(), $cteType = NULL )
  920. {
  921. if ( $leftRight !== 'LEFT' && $leftRight !== 'RIGHT' && $leftRight !== 'INNER' )
  922. throw new RedException( 'Invalid JOIN.' );
  923. $globalAliases = OODBBean::getAliases();
  924. if ( isset( $globalAliases[$targetType] ) ) {
  925. $destType = $globalAliases[$targetType];
  926. $asTargetTable = $this->esc( $targetType.$suffix );
  927. } else {
  928. $destType = $targetType;
  929. $asTargetTable = $this->esc( $destType.$suffix );
  930. }
  931. if ( $firstOfChain ) {
  932. $table = $this->esc( $type );
  933. } else {
  934. $table = $this->esc( $type.$suffix );
  935. }
  936. $targetTable = $this->esc( $destType );
  937. if ( $joinType == 'shared' ) {
  938. if ( isset( $globalAliases[$type] ) ) {
  939. $field = $this->esc( $globalAliases[$type], TRUE );
  940. if ( $aliases && count( $aliases ) === 1 ) {
  941. $assocTable = reset( $aliases );
  942. } else {
  943. $assocTable = $this->getAssocTable( array( $cteType ? $cteType : $globalAliases[$type], $destType ) );
  944. }
  945. } else {
  946. $field = $this->esc( $type, TRUE );
  947. if ( $aliases && count( $aliases ) === 1 ) {
  948. $assocTable = reset( $aliases );
  949. } else {
  950. $assocTable = $this->getAssocTable( array( $cteType ? $cteType : $type, $destType ) );
  951. }
  952. }
  953. $linkTable = $this->esc( $assocTable );
  954. $asLinkTable = $this->esc( $assocTable.$suffix );
  955. $leftField = "id";
  956. $rightField = $cteType ? "{$cteType}_id" : "{$field}_id";
  957. $linkField = $this->esc( $destType, TRUE );
  958. $linkLeftField = "id";
  959. $linkRightField = "{$linkField}_id";
  960. $joinSql = " {$leftRight} JOIN {$linkTable}";
  961. if ( isset( $globalAliases[$targetType] ) || $suffix ) {
  962. $joinSql .= " AS {$asLinkTable}";
  963. }
  964. $joinSql .= " ON {$table}.{$leftField} = {$asLinkTable}.{$rightField}";
  965. $joinSql .= " {$leftRight} JOIN {$targetTable}";
  966. if ( isset( $globalAliases[$targetType] ) || $suffix ) {
  967. $joinSql .= " AS {$asTargetTable}";
  968. }
  969. $joinSql .= " ON {$asTargetTable}.{$linkLeftField} = {$asLinkTable}.{$linkRightField}";
  970. } elseif ( $joinType == 'own' ) {
  971. $field = $this->esc( $type, TRUE );
  972. $rightField = "id";
  973. $joinSql = " {$leftRight} JOIN {$targetTable}";
  974. if ( isset( $globalAliases[$targetType] ) || $suffix ) {
  975. $joinSql .= " AS {$asTargetTable}";
  976. }
  977. if ( $aliases ) {
  978. $conditions = array();
  979. foreach ( $aliases as $alias ) {
  980. $conditions[] = "{$asTargetTable}.{$alias}_id = {$table}.{$rightField}";
  981. }
  982. $joinSql .= " ON ( " . implode( ' OR ', $conditions ) . " ) ";
  983. } else {
  984. $leftField = $cteType ? "{$cteType}_id" : "{$field}_id";
  985. $joinSql .= " ON {$asTargetTable}.{$leftField} = {$table}.{$rightField} ";
  986. }
  987. } else {
  988. $field = $this->esc( $targetType, TRUE );
  989. $leftField = "id";
  990. $joinSql = " {$leftRight} JOIN {$targetTable}";
  991. if ( isset( $globalAliases[$targetType] ) || $suffix ) {
  992. $joinSql .= " AS {$asTargetTable}";
  993. }
  994. if ( $aliases ) {
  995. $conditions = array();
  996. foreach ( $aliases as $alias ) {
  997. $conditions[] = "{$asTargetTable}.{$leftField} = {$table}.{$alias}_id";
  998. }
  999. $joinSql .= " ON ( " . implode( ' OR ', $conditions ) . " ) ";
  1000. } else {
  1001. $rightField = "{$field}_id";
  1002. $joinSql .= " ON {$asTargetTable}.{$leftField} = {$table}.{$rightField} ";
  1003. }
  1004. }
  1005. return $joinSql;
  1006. }
  1007. /**
  1008. * Sets an SQL snippet to be used for the next queryRecord() operation.
  1009. * A select snippet will be inserted at the end of the SQL select statement and
  1010. * can be used to modify SQL-select commands to enable locking, for instance
  1011. * using the 'FOR UPDATE' snippet (this will generate an SQL query like:
  1012. * 'SELECT * FROM ... FOR UPDATE'. After the query has been executed the
  1013. * SQL snippet will be erased. Note that only the first upcoming direct or
  1014. * indirect invocation of queryRecord() through batch(), find() or load()
  1015. * will be affected. The SQL snippet will be cached.
  1016. *
  1017. * @param string $sql SQL snippet to use in SELECT statement.
  1018. *
  1019. * return self
  1020. */
  1021. public function setSQLSelectSnippet( $sqlSelectSnippet = '' ) {
  1022. $this->sqlSelectSnippet = $sqlSelectSnippet;
  1023. return $this;
  1024. }
  1025. /**
  1026. * @see QueryWriter::queryRecord
  1027. */
  1028. public function queryRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() )
  1029. {
  1030. if ( $this->flagUseCache && $this->sqlSelectSnippet != self::C_SELECT_SNIPPET_FOR_UPDATE ) {
  1031. $key = $this->getCacheKey( array( $conditions, trim("$addSql {$this->sqlSelectSnippet}"), $bindings, 'select' ) );
  1032. if ( $cached = $this->getCached( $type, $key ) ) {
  1033. return $cached;
  1034. }
  1035. }
  1036. $table = $this->esc( $type );
  1037. $sqlFilterStr = '';
  1038. if ( count( self::$sqlFilters ) ) {
  1039. $sqlFilterStr = $this->getSQLFilterSnippet( $type );
  1040. }
  1041. if ( is_array ( $conditions ) && !empty ( $conditions ) ) {
  1042. $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql );
  1043. } else {
  1044. $sql = $this->glueSQLCondition( $addSql );
  1045. }
  1046. $sql = $this->parseJoin( $type, $sql );
  1047. $fieldSelection = self::$flagNarrowFieldMode ? "{$table}.*" : '*';
  1048. $sql = "SELECT {$fieldSelection} {$sqlFilterStr} FROM {$table} {$sql} {$this->sqlSelectSnippet} -- keep-cache";
  1049. $this->sqlSelectSnippet = '';
  1050. $rows = $this->adapter->get( $sql, $bindings );
  1051. if ( $this->flagUseCache && !empty( $key ) ) {
  1052. $this->putResultInCache( $type, $key, $rows );
  1053. }
  1054. return $rows;
  1055. }
  1056. /**
  1057. * @see QueryWriter::queryRecordWithCursor
  1058. */
  1059. public function queryRecordWithCursor( $type, $addSql = NULL, $bindings = array() )
  1060. {
  1061. $table = $this->esc( $type );
  1062. $sqlFilterStr = '';
  1063. if ( count( self::$sqlFilters ) ) {
  1064. $sqlFilterStr = $this->getSQLFilterSnippet( $type );
  1065. }
  1066. $sql = $this->glueSQLCondition( $addSql, NULL );
  1067. $sql = $this->parseJoin( $type, $sql );
  1068. $fieldSelection = self::$flagNarrowFieldMode ? "{$table}.*" : '*';
  1069. $sql = "SELECT {$fieldSelection} {$sqlFilterStr} FROM {$table} {$sql} -- keep-cache";
  1070. return $this->adapter->getCursor( $sql, $bindings );
  1071. }
  1072. /**
  1073. * @see QueryWriter::queryRecordRelated
  1074. */
  1075. public function queryRecordRelated( $sourceType, $destType, $linkIDs, $addSql = '', $bindings = array() )
  1076. {
  1077. list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType );
  1078. if ( $this->flagUseCache ) {
  1079. $key = $this->getCacheKey( array( $sourceType, implode( ',', $linkIDs ), trim($addSql), $bindings, 'selectrelated' ) );
  1080. if ( $cached = $this->getCached( $destType, $key ) ) {
  1081. return $cached;
  1082. }
  1083. }
  1084. $addSql = $this->glueSQLCondition( $addSql, QueryWriter::C_GLUE_WHERE );
  1085. $inClause = $this->getParametersForInClause( $linkIDs, $bindings );
  1086. $sqlFilterStr = '';
  1087. if ( count( self::$sqlFilters ) ) {
  1088. $sqlFilterStr = $this->getSQLFilterSnippet( $destType );
  1089. }
  1090. if ( $sourceType === $destType ) {
  1091. $inClause2 = $this->getParametersForInClause( $linkIDs, $bindings, count( $bindings ) ); //for some databases
  1092. $sql = "
  1093. SELECT
  1094. {$destTable}.* {$sqlFilterStr} ,
  1095. COALESCE(
  1096. NULLIF({$linkTable}.{$sourceCol}, {$destTable}.id),
  1097. NULLIF({$linkTable}.{$destCol}, {$destTable}.id)) AS linked_by
  1098. FROM {$linkTable}
  1099. INNER JOIN {$destTable} ON
  1100. ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} IN ($inClause) ) OR
  1101. ( {$destTable}.id = {$linkTable}.{$sourceCol} AND {$linkTable}.{$destCol} IN ($inClause2) )
  1102. {$addSql}
  1103. -- keep-cache";
  1104. $linkIDs = array_merge( $linkIDs, $linkIDs );
  1105. } else {
  1106. $sql = "
  1107. SELECT
  1108. {$destTable}.* {$sqlFilterStr},
  1109. {$linkTable}.{$sourceCol} AS linked_by
  1110. FROM {$linkTable}
  1111. INNER JOIN {$destTable} ON
  1112. ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} IN ($inClause) )
  1113. {$addSql}
  1114. -- keep-cache";
  1115. }
  1116. $bindings = array_merge( $linkIDs, $bindings );
  1117. $rows = $this->adapter->get( $sql, $bindings );
  1118. if ( $this->flagUseCache ) {
  1119. $this->putResultInCache( $destType, $key, $rows );
  1120. }
  1121. return $rows;
  1122. }
  1123. /**
  1124. * @see QueryWriter::queryRecordLink
  1125. */
  1126. public function queryRecordLink( $sourceType, $destType, $sourceID, $destID )
  1127. {
  1128. list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType );
  1129. if ( $this->flagUseCache ) {
  1130. $key = $this->getCacheKey( array( $sourceType, $destType, $sourceID, $destID, 'selectlink' ) );
  1131. if ( $cached = $this->getCached( $linkTable, $key ) ) {
  1132. return $cached;
  1133. }
  1134. }
  1135. $sqlFilterStr = '';
  1136. if ( count( self::$sqlFilters ) ) {
  1137. $sqlFilterStr = $this->getSQLFilterSnippet( $destType );
  1138. }
  1139. if ( $sourceTable === $destTable ) {
  1140. $sql = "SELECT {$linkTable}.* {$sqlFilterStr} FROM {$linkTable}
  1141. WHERE ( {$sourceCol} = ? AND {$destCol} = ? ) OR
  1142. ( {$destCol} = ? AND {$sourceCol} = ? ) -- keep-cache";
  1143. $row = $this->adapter->getRow( $sql, array( $sourceID, $destID, $sourceID, $destID ) );
  1144. } else {
  1145. $sql = "SELECT {$linkTable}.* {$sqlFilterStr} FROM {$linkTable}
  1146. WHERE {$sourceCol} = ? AND {$destCol} = ? -- keep-cache";
  1147. $row = $this->adapter->getRow( $sql, array( $sourceID, $destID ) );
  1148. }
  1149. if ( $this->flagUseCache ) {
  1150. $this->putResultInCache( $linkTable, $key, $row );
  1151. }
  1152. return $row;
  1153. }
  1154. /**
  1155. * Returns or counts all rows of specified type that have been tagged with one of the
  1156. * strings in the specified tag list array.
  1157. *
  1158. * Note that the additional SQL snippet can only be used for pagination,
  1159. * the SQL snippet will be appended to the end of the query.
  1160. *
  1161. * @param string $type the bean type you want to query
  1162. * @param array $tagList an array of strings, each string containing a tag title
  1163. * @param boolean $all if TRUE only return records that have been associated with ALL the tags in the list
  1164. * @param string $addSql addition SQL snippet, for pagination
  1165. * @param array $bindings parameter bindings for additional SQL snippet
  1166. * @param string $wrap SQL wrapper string (use %s for subquery)
  1167. *
  1168. * @return array
  1169. */
  1170. private function queryTaggedGeneric( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array(), $wrap = '%s' )
  1171. {
  1172. if ( $this->flagUseCache ) {
  1173. $key = $this->getCacheKey( array( implode( ',', $tagList ), $all, trim($addSql), $bindings, 'selectTagged' ) );
  1174. if ( $cached = $this->getCached( $type, $key ) ) {
  1175. return $cached;
  1176. }
  1177. }
  1178. $assocType = $this->getAssocTable( array( $type, 'tag' ) );
  1179. $assocTable = $this->esc( $assocType );
  1180. $assocField = $type . '_id';
  1181. $table = $this->esc( $type );
  1182. $slots = implode( ',', array_fill( 0, count( $tagList ), '?' ) );
  1183. $score = ( $all ) ? count( $tagList ) : 1;
  1184. $sql = "
  1185. SELECT {$table}.* FROM {$table}
  1186. INNER JOIN {$assocTable} ON {$assocField} = {$table}.id
  1187. INNER JOIN tag ON {$assocTable}.tag_id = tag.id
  1188. WHERE tag.title IN ({$slots})
  1189. GROUP BY {$table}.id
  1190. HAVING count({$table}.id) >= ?
  1191. {$addSql}
  1192. -- keep-cache
  1193. ";
  1194. $sql = sprintf($wrap,$sql);
  1195. $bindings = array_merge( $tagList, array( $score ), $bindings );
  1196. $rows = $this->adapter->get( $sql, $bindings );
  1197. if ( $this->flagUseCache ) {
  1198. $this->putResultInCache( $type, $key, $rows );
  1199. }
  1200. return $rows;
  1201. }
  1202. /**
  1203. * @see QueryWriter::queryTagged
  1204. */
  1205. public function queryTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() )
  1206. {
  1207. return $this->queryTaggedGeneric( $type, $tagList, $all, $addSql, $bindings );
  1208. }
  1209. /**
  1210. * @see QueryWriter::queryCountTagged
  1211. */
  1212. public function queryCountTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() )
  1213. {
  1214. $rows = $this->queryTaggedGeneric( $type, $tagList, $all, $addSql, $bindings, 'SELECT COUNT(*) AS counted FROM (%s) AS counting' );
  1215. return intval($rows[0]['counted']);
  1216. }
  1217. /**
  1218. * @see QueryWriter::queryRecordCount
  1219. */
  1220. public function queryRecordCount( $type, $conditions = array(), $addSql = NULL, $bindings = array() )
  1221. {
  1222. if ( $this->flagUseCache ) {
  1223. $key = $this->getCacheKey( array( $conditions, trim($addSql), $bindings, 'count' ) );
  1224. if ( $cached = $this->getCached( $type, $key ) ) {
  1225. return $cached;
  1226. }
  1227. }
  1228. $table = $this->esc( $type );
  1229. if ( is_array ( $conditions ) && !empty ( $conditions ) ) {
  1230. $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql );
  1231. } else {
  1232. $sql = $this->glueSQLCondition( $addSql );
  1233. }
  1234. $sql = $this->parseJoin( $type, $sql );
  1235. $sql = "SELECT COUNT(*) FROM {$table} {$sql} -- keep-cache";
  1236. $count = (int) $this->adapter->getCell( $sql, $bindings );
  1237. if ( $this->flagUseCache ) {
  1238. $this->putResultInCache( $type, $key, $count );
  1239. }
  1240. return $count;
  1241. }
  1242. /**
  1243. * @see QueryWriter::queryRecordCountRelated
  1244. */
  1245. public function queryRecordCountRelated( $sourceType, $destType, $linkID, $addSql = '', $bindings = array() )
  1246. {
  1247. list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType );
  1248. if ( $this->flagUseCache ) {
  1249. $cacheType = "#{$sourceType}/{$destType}";
  1250. $key = $this->getCacheKey( array( $sourceType, $destType, $linkID, trim($addSql), $bindings, 'countrelated' ) );
  1251. if ( $cached = $this->getCached( $cacheType, $key ) ) {
  1252. return $cached;
  1253. }
  1254. }
  1255. if ( $sourceType === $destType ) {
  1256. $sql = "
  1257. SELECT COUNT(*) FROM {$linkTable}
  1258. INNER JOIN {$destTable} ON
  1259. ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} = ? ) OR
  1260. ( {$destTable}.id = {$linkTable}.{$sourceCol} AND {$linkTable}.{$destCol} = ? )
  1261. {$addSql}
  1262. -- keep-cache";
  1263. $bindings = array_merge( array( $linkID, $linkID ), $bindings );
  1264. } else {
  1265. $sql = "
  1266. SELECT COUNT(*) FROM {$linkTable}
  1267. INNER JOIN {$destTable} ON
  1268. ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} = ? )
  1269. {$addSql}
  1270. -- keep-cache";
  1271. $bindings = array_merge( array( $linkID ), $bindings );
  1272. }
  1273. $count = (int) $this->adapter->getCell( $sql, $bindings );
  1274. if ( $this->flagUseCache ) {
  1275. $this->putResultInCache( $cacheType, $key, $count );
  1276. }
  1277. return $count;
  1278. }
  1279. /**
  1280. * @see QueryWriter::queryRecursiveCommonTableExpression
  1281. */
  1282. public function queryRecursiveCommonTableExpression( $type, $id, $up = TRUE, $addSql = NULL, $bindings = array(), $count = FALSE )
  1283. {
  1284. $alias = $up ? 'parent' : 'child';
  1285. $direction = $up ? " {$alias}.{$type}_id = {$type}.id " : " {$alias}.id = {$type}.{$type}_id ";
  1286. /* allow numeric and named param bindings, if '0' exists then numeric */
  1287. if ( array_key_exists( 0,$bindings ) ) {
  1288. array_unshift( $bindings, $id );
  1289. $idSlot = '?';
  1290. } else {
  1291. $idSlot = ':slot0';
  1292. $bindings[$idSlot] = $id;
  1293. }
  1294. $sql = $this->glueSQLCondition( $addSql, QueryWriter::C_GLUE_WHERE );
  1295. $sql = $this->parseJoin( 'redbeantree', $sql, $type );
  1296. $rows = $this->adapter->get("
  1297. WITH RECURSIVE redbeantree AS
  1298. (
  1299. SELECT *
  1300. FROM {$type} WHERE {$type}.id = {$idSlot}
  1301. UNION ALL
  1302. SELECT {$type}.* FROM {$type}
  1303. INNER JOIN redbeantree {$alias} ON {$direction}
  1304. )
  1305. SELECT ".($count ? "count(redbeantree.*)" : "redbeantree.*" )." FROM redbeantree {$sql};",
  1306. $bindings
  1307. );
  1308. return $rows;
  1309. }
  1310. /**
  1311. * @see QueryWriter::deleteRecord
  1312. */
  1313. public function deleteRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() )
  1314. {
  1315. $table = $this->esc( $type );
  1316. if ( is_array ( $conditions ) && !empty ( $conditions ) ) {
  1317. $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql );
  1318. } else {
  1319. $sql = $this->glueSQLCondition( $addSql );
  1320. }
  1321. $sql = "DELETE FROM {$table} {$sql}";
  1322. return $this->adapter->exec( $sql, $bindings );
  1323. }
  1324. /**
  1325. * @see QueryWriter::deleteRelations
  1326. */
  1327. public function deleteRelations( $sourceType, $destType, $sourceID )
  1328. {
  1329. list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType );
  1330. if ( $sourceTable === $destTable ) {
  1331. $sql = "DELETE FROM {$linkTable}
  1332. WHERE ( {$sourceCol} = ? ) OR
  1333. ( {$destCol} = ? )
  1334. ";
  1335. $this->adapter->exec( $sql, array( $sourceID, $sourceID ) );
  1336. } else {
  1337. $sql = "DELETE FROM {$linkTable}
  1338. WHERE {$sourceCol} = ? ";
  1339. $this->adapter->exec( $sql, array( $sourceID ) );
  1340. }
  1341. }
  1342. /**
  1343. * @see QueryWriter::widenColumn
  1344. */
  1345. public function widenColumn( $type, $property, $dataType )
  1346. {
  1347. if ( !isset($this->typeno_sqltype[$dataType]) ) return FALSE;
  1348. $table = $this->esc( $type );
  1349. $column = $this->esc( $property );
  1350. $newType = $this->typeno_sqltype[$dataType];
  1351. $this->adapter->exec( sprintf( $this->getDDLTemplate( 'widenColumn', $type, $column ), $type, $column, $column, $newType ) );
  1352. return TRUE;
  1353. }
  1354. /**
  1355. * @see QueryWriter::wipe
  1356. */
  1357. public function wipe( $type )
  1358. {
  1359. $table = $this->esc( $type );
  1360. $this->adapter->exec( "TRUNCATE $table " );
  1361. }
  1362. /**
  1363. * @see QueryWriter::renameAssocTable
  1364. */
  1365. public function renameAssocTable( $from, $to = NULL )
  1366. {
  1367. self::renameAssociation( $from, $to );
  1368. }
  1369. /**
  1370. * @see QueryWriter::getAssocTable
  1371. */
  1372. public function getAssocTable( $types )
  1373. {
  1374. return self::getAssocTableFormat( $types );
  1375. }
  1376. /**
  1377. * Turns caching on or off. Default: off.
  1378. * If caching is turned on retrieval queries fired after eachother will
  1379. * use a result row cache.
  1380. *
  1381. * @param boolean
  1382. *
  1383. * @return void
  1384. */
  1385. public function setUseCache( $yesNo )
  1386. {
  1387. $this->flushCache();
  1388. $this->flagUseCache = (bool) $yesNo;
  1389. }
  1390. /**
  1391. * Flushes the Query Writer Cache.
  1392. * Clears the internal query cache array and returns its overall
  1393. * size.
  1394. *
  1395. * @return mixed
  1396. */
  1397. public function flushCache( $newMaxCacheSizePerType = NULL, $countCache = TRUE )
  1398. {
  1399. if ( !is_null( $newMaxCacheSizePerType ) && $newMaxCacheSizePerType > 0 ) {
  1400. $this->maxCacheSizePerType = $newMaxCacheSizePerType;
  1401. }
  1402. $count = $countCache ? count( $this->cache, COUNT_RECURSIVE ) : NULL;
  1403. $this->cache = array();
  1404. return $count;
  1405. }
  1406. /**
  1407. * @deprecated Use esc() instead.
  1408. *
  1409. * @param string $column column to be escaped
  1410. * @param boolean $noQuotes omit quotes
  1411. *
  1412. * @return string
  1413. */
  1414. public function safeColumn( $column, $noQuotes = FALSE )
  1415. {
  1416. return $this->esc( $column, $noQuotes );
  1417. }
  1418. /**
  1419. * @deprecated Use esc() instead.
  1420. *
  1421. * @param string $table table to be escaped
  1422. * @param boolean $noQuotes omit quotes
  1423. *
  1424. * @return string
  1425. */
  1426. public function safeTable( $table, $noQuotes = FALSE )
  1427. {
  1428. return $this->esc( $table, $noQuotes );
  1429. }
  1430. /**
  1431. * @see QueryWriter::addUniqueConstraint
  1432. */
  1433. public function addUniqueIndex( $type, $properties )
  1434. {
  1435. return $this->addUniqueConstraint( $type, $properties );
  1436. }
  1437. }