PageRenderTime 38ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/RedBeanPHP/QueryWriter/MySQL.php

https://github.com/gabordemooij/redbean
PHP | 460 lines | 295 code | 50 blank | 115 comment | 46 complexity | 9d63ca555068a2011b4aca7bd406a65c MD5 | raw file
  1. <?php
  2. namespace RedBeanPHP\QueryWriter;
  3. use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter;
  4. use RedBeanPHP\QueryWriter as QueryWriter;
  5. use RedBeanPHP\Adapter\DBAdapter as DBAdapter;
  6. use RedBeanPHP\Adapter as Adapter;
  7. use RedBeanPHP\RedException\SQL as SQLException;
  8. /**
  9. * RedBeanPHP MySQLWriter.
  10. * This is a QueryWriter class for RedBeanPHP.
  11. * This QueryWriter provides support for the MySQL/MariaDB database platform.
  12. *
  13. * @file RedBeanPHP/QueryWriter/MySQL.php
  14. * @author Gabor de Mooij and the RedBeanPHP Community
  15. * @license BSD/GPLv2
  16. *
  17. * @copyright
  18. * (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community.
  19. * This source file is subject to the BSD/GPLv2 License that is bundled
  20. * with this source code in the file license.txt.
  21. */
  22. class MySQL extends AQueryWriter implements QueryWriter
  23. {
  24. /**
  25. * Data types
  26. */
  27. const C_DATATYPE_BOOL = 0;
  28. const C_DATATYPE_UINT32 = 2;
  29. const C_DATATYPE_DOUBLE = 3;
  30. const C_DATATYPE_TEXT7 = 4; //InnoDB cant index varchar(255) utf8mb4 - so keep 191 as long as possible
  31. const C_DATATYPE_TEXT8 = 5;
  32. const C_DATATYPE_TEXT16 = 6;
  33. const C_DATATYPE_TEXT32 = 7;
  34. const C_DATATYPE_SPECIAL_DATE = 80;
  35. const C_DATATYPE_SPECIAL_DATETIME = 81;
  36. const C_DATATYPE_SPECIAL_TIME = 83; //MySQL time column (only manual)
  37. const C_DATATYPE_SPECIAL_POINT = 90;
  38. const C_DATATYPE_SPECIAL_LINESTRING = 91;
  39. const C_DATATYPE_SPECIAL_POLYGON = 92;
  40. const C_DATATYPE_SPECIAL_MONEY = 93;
  41. const C_DATATYPE_SPECIAL_JSON = 94; //JSON support (only manual)
  42. const C_DATATYPE_SPECIFIED = 99;
  43. /**
  44. * @var DBAdapter
  45. */
  46. protected $adapter;
  47. /**
  48. * @var string
  49. */
  50. protected $quoteCharacter = '`';
  51. /**
  52. * @var array
  53. */
  54. protected $DDLTemplates = array(
  55. 'addColumn' => array(
  56. '*' => 'ALTER TABLE %s ADD %s %s '
  57. ),
  58. 'createTable' => array(
  59. '*' => 'CREATE TABLE %s (id INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY ( id )) ENGINE = InnoDB DEFAULT CHARSET=%s COLLATE=%s '
  60. ),
  61. 'widenColumn' => array(
  62. '*' => 'ALTER TABLE `%s` CHANGE %s %s %s '
  63. )
  64. );
  65. /**
  66. * @see AQueryWriter::getKeyMapForType
  67. */
  68. protected function getKeyMapForType( $type )
  69. {
  70. $databaseName = $this->adapter->getCell('SELECT DATABASE()');
  71. $table = $this->esc( $type, TRUE );
  72. $keys = $this->adapter->get('
  73. SELECT
  74. information_schema.key_column_usage.constraint_name AS `name`,
  75. information_schema.key_column_usage.referenced_table_name AS `table`,
  76. information_schema.key_column_usage.column_name AS `from`,
  77. information_schema.key_column_usage.referenced_column_name AS `to`,
  78. information_schema.referential_constraints.update_rule AS `on_update`,
  79. information_schema.referential_constraints.delete_rule AS `on_delete`
  80. FROM information_schema.key_column_usage
  81. INNER JOIN information_schema.referential_constraints
  82. ON information_schema.referential_constraints.constraint_name = information_schema.key_column_usage.constraint_name
  83. WHERE
  84. information_schema.key_column_usage.table_schema = :database
  85. AND information_schema.referential_constraints.constraint_schema = :database
  86. AND information_schema.key_column_usage.constraint_schema = :database
  87. AND information_schema.key_column_usage.table_name = :table
  88. AND information_schema.key_column_usage.constraint_name != \'PRIMARY\'
  89. AND information_schema.key_column_usage.referenced_table_name IS NOT NULL
  90. ', array( ':database' => $databaseName, ':table' => $table ) );
  91. $keyInfoList = array();
  92. foreach ( $keys as $k ) {
  93. $label = $this->makeFKLabel( $k['from'], $k['table'], $k['to'] );
  94. $keyInfoList[$label] = array(
  95. 'name' => $k['name'],
  96. 'from' => $k['from'],
  97. 'table' => $k['table'],
  98. 'to' => $k['to'],
  99. 'on_update' => $k['on_update'],
  100. 'on_delete' => $k['on_delete']
  101. );
  102. }
  103. return $keyInfoList;
  104. }
  105. /**
  106. * Constructor
  107. * Most of the time, you do not need to use this constructor,
  108. * since the facade takes care of constructing and wiring the
  109. * RedBeanPHP core objects. However if you would like to
  110. * assemble an OODB instance yourself, this is how it works:
  111. *
  112. * Usage:
  113. *
  114. * <code>
  115. * $database = new RPDO( $dsn, $user, $pass );
  116. * $adapter = new DBAdapter( $database );
  117. * $writer = new PostgresWriter( $adapter );
  118. * $oodb = new OODB( $writer, FALSE );
  119. * $bean = $oodb->dispense( 'bean' );
  120. * $bean->name = 'coffeeBean';
  121. * $id = $oodb->store( $bean );
  122. * $bean = $oodb->load( 'bean', $id );
  123. * </code>
  124. *
  125. * The example above creates the 3 RedBeanPHP core objects:
  126. * the Adapter, the Query Writer and the OODB instance and
  127. * wires them together. The example also demonstrates some of
  128. * the methods that can be used with OODB, as you see, they
  129. * closely resemble their facade counterparts.
  130. *
  131. * The wiring process: create an RPDO instance using your database
  132. * connection parameters. Create a database adapter from the RPDO
  133. * object and pass that to the constructor of the writer. Next,
  134. * create an OODB instance from the writer. Now you have an OODB
  135. * object.
  136. *
  137. * @param Adapter $adapter Database Adapter
  138. * @param array $options options array
  139. */
  140. public function __construct( Adapter $adapter, $options = array() )
  141. {
  142. $this->typeno_sqltype = array(
  143. MySQL::C_DATATYPE_BOOL => ' TINYINT(1) UNSIGNED ',
  144. MySQL::C_DATATYPE_UINT32 => ' INT(11) UNSIGNED ',
  145. MySQL::C_DATATYPE_DOUBLE => ' DOUBLE ',
  146. MySQL::C_DATATYPE_TEXT7 => ' VARCHAR(191) ',
  147. MYSQL::C_DATATYPE_TEXT8 => ' VARCHAR(255) ',
  148. MySQL::C_DATATYPE_TEXT16 => ' TEXT ',
  149. MySQL::C_DATATYPE_TEXT32 => ' LONGTEXT ',
  150. MySQL::C_DATATYPE_SPECIAL_DATE => ' DATE ',
  151. MySQL::C_DATATYPE_SPECIAL_DATETIME => ' DATETIME ',
  152. MySQL::C_DATATYPE_SPECIAL_TIME => ' TIME ',
  153. MySQL::C_DATATYPE_SPECIAL_POINT => ' POINT ',
  154. MySQL::C_DATATYPE_SPECIAL_LINESTRING => ' LINESTRING ',
  155. MySQL::C_DATATYPE_SPECIAL_POLYGON => ' POLYGON ',
  156. MySQL::C_DATATYPE_SPECIAL_MONEY => ' DECIMAL(10,2) ',
  157. MYSQL::C_DATATYPE_SPECIAL_JSON => ' JSON '
  158. );
  159. $this->sqltype_typeno = array();
  160. foreach ( $this->typeno_sqltype as $k => $v ) {
  161. $this->sqltype_typeno[trim( strtolower( $v ) )] = $k;
  162. }
  163. $this->adapter = $adapter;
  164. $this->encoding = $this->adapter->getDatabase()->getMysqlEncoding();
  165. $me = $this;
  166. if (!isset($options['noInitcode']))
  167. $this->adapter->setInitCode(function($version) use(&$me) {
  168. try {
  169. if (strpos($version, 'maria')===FALSE && intval($version)>=8) {
  170. $me->useFeature('ignoreDisplayWidth');
  171. }
  172. } catch( \Exception $e ){}
  173. });
  174. }
  175. /**
  176. * Enables certain features/dialects.
  177. *
  178. * - ignoreDisplayWidth required for MySQL8+
  179. * (automatically set by setup() if you pass dsn instead of PDO object)
  180. *
  181. * @param string $name feature ID
  182. *
  183. * @return void
  184. */
  185. public function useFeature($name) {
  186. if ($name == 'ignoreDisplayWidth') {
  187. $this->typeno_sqltype[MySQL::C_DATATYPE_BOOL] = ' TINYINT UNSIGNED ';
  188. $this->typeno_sqltype[MySQL::C_DATATYPE_UINT32] = ' INT UNSIGNED ';
  189. foreach ( $this->typeno_sqltype as $k => $v ) {
  190. $this->sqltype_typeno[trim( strtolower( $v ) )] = $k;
  191. }
  192. }
  193. }
  194. /**
  195. * This method returns the datatype to be used for primary key IDS and
  196. * foreign keys. Returns one if the data type constants.
  197. *
  198. * @return integer
  199. */
  200. public function getTypeForID()
  201. {
  202. return self::C_DATATYPE_UINT32;
  203. }
  204. /**
  205. * @see QueryWriter::getTables
  206. */
  207. public function getTables()
  208. {
  209. return $this->adapter->getCol( 'show tables' );
  210. }
  211. /**
  212. * @see QueryWriter::createTable
  213. */
  214. public function createTable( $type )
  215. {
  216. $table = $this->esc( $type );
  217. $charset_collate = $this->adapter->getDatabase()->getMysqlEncoding( TRUE );
  218. $charset = $charset_collate['charset'];
  219. $collate = $charset_collate['collate'];
  220. $sql = sprintf( $this->getDDLTemplate( 'createTable', $type ), $table, $charset, $collate );
  221. $this->adapter->exec( $sql );
  222. }
  223. /**
  224. * @see QueryWriter::getColumns
  225. */
  226. public function getColumns( $table )
  227. {
  228. $columnsRaw = $this->adapter->get( "DESCRIBE " . $this->esc( $table ) );
  229. $columns = array();
  230. foreach ( $columnsRaw as $r ) {
  231. $columns[$r['Field']] = $r['Type'];
  232. }
  233. return $columns;
  234. }
  235. /**
  236. * @see QueryWriter::scanType
  237. */
  238. public function scanType( $value, $flagSpecial = FALSE )
  239. {
  240. $this->svalue = $value;
  241. if ( is_null( $value ) ) return MySQL::C_DATATYPE_BOOL;
  242. if ( $value === INF ) return MySQL::C_DATATYPE_TEXT7;
  243. if ( $flagSpecial ) {
  244. if ( preg_match( '/^-?\d+\.\d{2}$/', $value ) ) {
  245. return MySQL::C_DATATYPE_SPECIAL_MONEY;
  246. }
  247. if ( preg_match( '/^\d{4}\-\d\d-\d\d$/', $value ) ) {
  248. return MySQL::C_DATATYPE_SPECIAL_DATE;
  249. }
  250. if ( preg_match( '/^\d{4}\-\d\d-\d\d\s\d\d:\d\d:\d\d$/', $value ) ) {
  251. return MySQL::C_DATATYPE_SPECIAL_DATETIME;
  252. }
  253. if ( preg_match( '/^POINT\(/', $value ) ) {
  254. return MySQL::C_DATATYPE_SPECIAL_POINT;
  255. }
  256. if ( preg_match( '/^LINESTRING\(/', $value ) ) {
  257. return MySQL::C_DATATYPE_SPECIAL_LINESTRING;
  258. }
  259. if ( preg_match( '/^POLYGON\(/', $value ) ) {
  260. return MySQL::C_DATATYPE_SPECIAL_POLYGON;
  261. }
  262. if ( self::$flagUseJSONColumns && $this->isJSON( $value ) ) {
  263. return self::C_DATATYPE_SPECIAL_JSON;
  264. }
  265. }
  266. //setter turns TRUE FALSE into 0 and 1 because database has no real bools (TRUE and FALSE only for test?).
  267. if ( $value === FALSE || $value === TRUE || $value === '0' || $value === '1' || $value === 0 || $value === 1 ) {
  268. return MySQL::C_DATATYPE_BOOL;
  269. }
  270. if ( is_float( $value ) ) return self::C_DATATYPE_DOUBLE;
  271. if ( !$this->startsWithZeros( $value ) ) {
  272. if ( is_numeric( $value ) && ( floor( $value ) == $value ) && $value >= 0 && $value <= 4294967295 ) {
  273. return MySQL::C_DATATYPE_UINT32;
  274. }
  275. if ( is_numeric( $value ) ) {
  276. return MySQL::C_DATATYPE_DOUBLE;
  277. }
  278. }
  279. if ( mb_strlen( $value, 'UTF-8' ) <= 191 ) {
  280. return MySQL::C_DATATYPE_TEXT7;
  281. }
  282. if ( mb_strlen( $value, 'UTF-8' ) <= 255 ) {
  283. return MySQL::C_DATATYPE_TEXT8;
  284. }
  285. if ( mb_strlen( $value, 'UTF-8' ) <= 65535 ) {
  286. return MySQL::C_DATATYPE_TEXT16;
  287. }
  288. return MySQL::C_DATATYPE_TEXT32;
  289. }
  290. /**
  291. * @see QueryWriter::code
  292. */
  293. public function code( $typedescription, $includeSpecials = FALSE )
  294. {
  295. if ( isset( $this->sqltype_typeno[$typedescription] ) ) {
  296. $r = $this->sqltype_typeno[$typedescription];
  297. } else {
  298. $r = self::C_DATATYPE_SPECIFIED;
  299. }
  300. if ( $includeSpecials ) {
  301. return $r;
  302. }
  303. if ( $r >= QueryWriter::C_DATATYPE_RANGE_SPECIAL ) {
  304. return self::C_DATATYPE_SPECIFIED;
  305. }
  306. return $r;
  307. }
  308. /**
  309. * @see QueryWriter::addUniqueIndex
  310. */
  311. public function addUniqueConstraint( $type, $properties )
  312. {
  313. $tableNoQ = $this->esc( $type, TRUE );
  314. $columns = array();
  315. foreach( $properties as $key => $column ) $columns[$key] = $this->esc( $column );
  316. $table = $this->esc( $type );
  317. sort( $columns ); // Else we get multiple indexes due to order-effects
  318. $name = 'UQ_' . sha1( implode( ',', $columns ) );
  319. try {
  320. $sql = "ALTER TABLE $table
  321. ADD UNIQUE INDEX $name (" . implode( ',', $columns ) . ")";
  322. $this->adapter->exec( $sql );
  323. } catch ( SQLException $e ) {
  324. //do nothing, dont use alter table ignore, this will delete duplicate records in 3-ways!
  325. return FALSE;
  326. }
  327. return TRUE;
  328. }
  329. /**
  330. * @see QueryWriter::addIndex
  331. */
  332. public function addIndex( $type, $name, $property )
  333. {
  334. try {
  335. $table = $this->esc( $type );
  336. $name = preg_replace( '/\W/', '', $name );
  337. $column = $this->esc( $property );
  338. $this->adapter->exec( "CREATE INDEX $name ON $table ($column) " );
  339. return TRUE;
  340. } catch ( SQLException $e ) {
  341. return FALSE;
  342. }
  343. }
  344. /**
  345. * @see QueryWriter::addFK
  346. * @return bool
  347. */
  348. public function addFK( $type, $targetType, $property, $targetProperty, $isDependent = FALSE )
  349. {
  350. $table = $this->esc( $type );
  351. $targetTable = $this->esc( $targetType );
  352. $targetTableNoQ = $this->esc( $targetType, TRUE );
  353. $field = $this->esc( $property );
  354. $fieldNoQ = $this->esc( $property, TRUE );
  355. $targetField = $this->esc( $targetProperty );
  356. $targetFieldNoQ = $this->esc( $targetProperty, TRUE );
  357. $tableNoQ = $this->esc( $type, TRUE );
  358. $fieldNoQ = $this->esc( $property, TRUE );
  359. if ( !is_null( $this->getForeignKeyForTypeProperty( $tableNoQ, $fieldNoQ ) ) ) return FALSE;
  360. //Widen the column if it's incapable of representing a foreign key (at least INT).
  361. $columns = $this->getColumns( $tableNoQ );
  362. $idType = $this->getTypeForID();
  363. if ( $this->code( $columns[$fieldNoQ] ) !== $idType ) {
  364. $this->widenColumn( $type, $property, $idType );
  365. }
  366. $fkName = 'fk_'.($tableNoQ.'_'.$fieldNoQ);
  367. $cName = 'c_'.$fkName;
  368. try {
  369. $this->adapter->exec( "
  370. ALTER TABLE {$table}
  371. ADD CONSTRAINT $cName
  372. FOREIGN KEY $fkName ( `{$fieldNoQ}` ) REFERENCES `{$targetTableNoQ}`
  373. (`{$targetFieldNoQ}`) ON DELETE " . ( $isDependent ? 'CASCADE' : 'SET NULL' ) . ' ON UPDATE '.( $isDependent ? 'CASCADE' : 'SET NULL' ).';');
  374. } catch ( SQLException $e ) {
  375. // Failure of fk-constraints is not a problem
  376. }
  377. return TRUE;
  378. }
  379. /**
  380. * @see QueryWriter::sqlStateIn
  381. */
  382. public function sqlStateIn( $state, $list, $extraDriverDetails = array() )
  383. {
  384. $stateMap = array(
  385. '42S02' => QueryWriter::C_SQLSTATE_NO_SUCH_TABLE,
  386. '42S22' => QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN,
  387. '23000' => QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION,
  388. );
  389. if ( $state == 'HY000' && !empty( $extraDriverDetails[1] ) ) {
  390. $driverCode = $extraDriverDetails[1];
  391. if ( $driverCode == '1205' && in_array( QueryWriter::C_SQLSTATE_LOCK_TIMEOUT, $list ) ) {
  392. return TRUE;
  393. }
  394. }
  395. return in_array( ( isset( $stateMap[$state] ) ? $stateMap[$state] : '0' ), $list );
  396. }
  397. /**
  398. * @see QueryWriter::wipeAll
  399. */
  400. public function wipeAll()
  401. {
  402. if (AQueryWriter::$noNuke) throw new \Exception('The nuke() command has been disabled using noNuke() or R::feature(novice/...).');
  403. $this->adapter->exec( 'SET FOREIGN_KEY_CHECKS = 0;' );
  404. foreach ( $this->getTables() as $t ) {
  405. try { $this->adapter->exec( "DROP TABLE IF EXISTS `$t`" ); } catch ( SQLException $e ) { ; }
  406. try { $this->adapter->exec( "DROP VIEW IF EXISTS `$t`" ); } catch ( SQLException $e ) { ; }
  407. }
  408. $this->adapter->exec( 'SET FOREIGN_KEY_CHECKS = 1;' );
  409. }
  410. }