DB_StructUpdater.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769
  1. <?php
  2. /**
  3. TODO: make it work even without ';' delimiters or at least warn about that
  4. TODO: better parse error reporting
  5. TODO: accept empty datetime value and 0000-00-00 00:00:00 are equal, similar with date and time, also enum('0','1') [default 0], what's with floats?(float(10,2) NOT NULL default '0.00'); text,mediumtext,etc;
  6. TODO: option to add database name with dot before the table names
  7. TODO: add option "order does matter"
  8. DONE: breaks table definition on commas and brackets, not newlines
  9. DONE: handles `database`.`table` in CREATE TABLE string (but does not add database to result sql for a while - and if it
  10. should? as same tables struct in 2 DBs compared is also a case)
  11. DONE: handles double (and more) spaces in CREATE TABLE string
  12. DONE: add filter option (fields: MODIFY, ADD, DROP, tables: CREATE, DROP)
  13. DONE: make it work also with comments
  14. DONE: move all options to $this->config
  15. */
  16. /**
  17. * The class provides ability to compare 2 database structure dumps and compile a set of sql statements to update
  18. * one database to make it structure identical to another.
  19. *
  20. * @author Kirill Gerasimenko <ger.kirill@gmail.com>
  21. *
  22. * The input for the script could be taken from the phpMyAdmin structure dump, or provided by some custom code
  23. * that uses 'SHOW CREATE TABLE' query to get database structure table by table.
  24. * The output is either array of sql statements suitable for executions right from php or a string where the
  25. * statements are placed each at new line and delimited with ';' - suitable for execution from phpMyAdmin SQL
  26. * page.
  27. * The resulting sql may contain queries that aim to:
  28. * Create missing table (CREATE TABLE query)
  29. * Delete table which should not longer exist (DROP TABLE query)
  30. * Update, drop or add table field or index definition (ALTER TABLE query)
  31. *
  32. * Some features:
  33. * - AUTO_INCREMENT value is ommited during the comparison and in resulting CREATE TABLE sql
  34. * - fields with definitions like "(var)char (255) NOT NULL default ''" and "(var)char (255) NOT NULL" are treated
  35. * as equal, the same for (big|tiny)int NOT NULL default 0;
  36. * - IF NOT EXISTS is automatically added to the resulting sql CREATE TABLE statement
  37. * - fields updating queries always come before key modification ones for each table
  38. * Not implemented:
  39. * - The class even does not try to insert or re-order fields in the same order as in the original table.
  40. * Does order matter?
  41. * IMPORTANT!!! Class will not handle a case when the field was renamed. It will generate 2 queries - one to drop
  42. * the column with the old name and one to create column with the new name, so if there is a data in the dropped
  43. * column, it will be lost.
  44. * Usage example:
  45. $updater = new DB_StructUpdater();
  46. $res = $updater->getUpdates($struct1, $struct2);
  47. -----
  48. $res == array (
  49. [0]=>"ALTER TABLE `b` MODIFY `name` varchar(255) NOT NULL",
  50. ...
  51. )
  52. */
  53. class DB_StructUpdater
  54. {
  55. var $sourceStruct = '';//structure dump of the reference database
  56. var $destStruct = '';//structure dump of database to update
  57. var $config = array();//updater configuration
  58. /**
  59. * Constructor
  60. * @access public
  61. */
  62. function __construct()
  63. {
  64. $this->init();
  65. }
  66. function init()
  67. {
  68. //table operations: create, drop; field operations: add, remove, modify
  69. $this->config['updateTypes'] = 'create, drop, add, remove, modify';
  70. //ignores default part in cases like (var)char NOT NULL default '' upon the comparison
  71. $this->config['varcharDefaultIgnore'] = true;
  72. //the same for int NOT NULL default 0
  73. $this->config['intDefaultIgnore'] = true;
  74. //ignores table autoincrement field value, also remove AUTO_INCREMENT value from the create query if exists
  75. $this->config['ignoreIncrement'] = true;
  76. //add 'IF NOT EXIST' to each CREATE TABLE query
  77. $this->config['forceIfNotExists'] = true;
  78. //remove 'IF NOT EXIST' if already exists CREATE TABLE dump
  79. $this->config['ingoreIfNotExists'] = false;
  80. }
  81. /**
  82. * merges current updater config with the given one
  83. * @param assoc_array $config new configuration values
  84. */
  85. function setConfig($config=array())
  86. {
  87. if (is_array($config))
  88. {
  89. $this->config = array_merge($this->config, $config);
  90. }
  91. }
  92. /**
  93. * Returns array of update SQL with default options, $source, $dest - database structures
  94. * @access public
  95. * @param string $source structure dump of database to update
  96. * @param string $dest structure dump of the reference database
  97. * @param bool $asString if true - result will be a string, otherwise - array
  98. * @return array|string update sql statements - in array or string (separated with ';')
  99. */
  100. function getUpdates($source, $dest, $asString=false)
  101. {
  102. $result = $asString?'':array();
  103. $compRes = $this->compare($source, $dest);
  104. if (empty($compRes))
  105. {
  106. return $result;
  107. }
  108. $compRes = $this->filterDiffs($compRes);
  109. if (empty($compRes))
  110. {
  111. return $result;
  112. }
  113. $result = $this->getDiffSql($compRes);
  114. if ($asString)
  115. {
  116. $result = implode(";\r\n", $result).';';
  117. }
  118. return $result;
  119. }
  120. /**
  121. * Filters comparison result and lefts only sync actions allowed by 'updateTypes' option
  122. */
  123. function filterDiffs($compRes)
  124. {
  125. $result = array();
  126. if (is_array($this->config['updateTypes']))
  127. {
  128. $updateActions = $this->config['updateTypes'];
  129. }
  130. else
  131. {
  132. $updateActions = array_map('trim', explode(',', $this->config['updateTypes']));
  133. }
  134. $allowedActions = array('create', 'drop', 'add', 'remove', 'modify');
  135. $updateActions = array_intersect($updateActions, $allowedActions);
  136. foreach($compRes as $table=>$info)
  137. {
  138. if ($info['sourceOrphan'])
  139. {
  140. if (in_array('create', $updateActions))
  141. {
  142. $result[$table] = $info;
  143. }
  144. }
  145. elseif ($info['destOrphan'])
  146. {
  147. if (in_array('drop', $updateActions))
  148. {
  149. $result[$table] = $info;
  150. }
  151. }
  152. elseif($info['differs'])
  153. {
  154. $resultInfo = $info;
  155. unset($resultInfo['differs']);
  156. foreach ($info['differs'] as $diff)
  157. {
  158. if (empty($diff['dest']) && in_array('add', $updateActions))
  159. {
  160. $resultInfo['differs'][] = $diff;
  161. }
  162. elseif (empty($diff['source']) && in_array('remove', $updateActions))
  163. {
  164. $resultInfo['differs'][] = $diff;
  165. }
  166. elseif(in_array('modify', $updateActions))
  167. {
  168. $resultInfo['differs'][] = $diff;
  169. }
  170. }
  171. if (!empty($resultInfo['differs']))
  172. {
  173. $result[$table] = $resultInfo;
  174. }
  175. }
  176. }
  177. return $result;
  178. }
  179. /**
  180. * Gets structured general info about the databases diff :
  181. * array(sourceOrphans=>array(...), destOrphans=>array(...), different=>array(...))
  182. */
  183. function getDiffInfo($compRes)
  184. {
  185. if (!is_array($compRes))
  186. {
  187. return false;
  188. }
  189. $result = array('sourceOrphans'=>array(), 'destOrphans'=>array(), 'different'=>array());
  190. foreach($compRes as $table=>$info)
  191. {
  192. if ($info['sourceOrphan'])
  193. {
  194. $result['sourceOrphans'][] = $table;
  195. }
  196. elseif ($info['destOrphan'])
  197. {
  198. $result['destOrphans'][] = $table;
  199. }
  200. else
  201. {
  202. $result['different'][] = $table;
  203. }
  204. }
  205. return $result;
  206. }
  207. /**
  208. * Makes comparison of the given database structures, support some options
  209. * @access private
  210. * @param string $source and $dest are strings - database tables structures
  211. * @return array
  212. * - table (array)
  213. * - destOrphan (boolean)
  214. * - sourceOrphan (boolean)
  215. * - differs (array) OR (boolean) false if no diffs
  216. * - [0](array)
  217. * - source (string) structure definition line in the out-of-date table
  218. * - dest (string) structure definition line in the reference table
  219. * - [1](array) ...
  220. */
  221. function compare($source, $dest)
  222. {
  223. $this->sourceStruct = $source;
  224. $this->destStruct = $dest;
  225. $result = array();
  226. $destTabNames = $this->getTableList($this->destStruct);
  227. $sourceTabNames = $this->getTableList($this->sourceStruct);
  228. $common = array_intersect($destTabNames, $sourceTabNames);
  229. $destOrphans = array_diff($destTabNames, $common);
  230. $sourceOrphans = array_diff($sourceTabNames, $common);
  231. $all = array_unique(array_merge($destTabNames, $sourceTabNames));
  232. sort($all);
  233. foreach ($all as $tab)
  234. {
  235. $info = array('destOrphan'=>false, 'sourceOrphan'=>false, 'differs'=>false);
  236. if(in_array($tab, $destOrphans))
  237. {
  238. $info['destOrphan'] = true;
  239. }
  240. elseif (in_array($tab, $sourceOrphans))
  241. {
  242. $info['sourceOrphan'] = true;
  243. }
  244. else
  245. {
  246. $destSql = $this->getTabSql($this->destStruct, $tab, true);
  247. $sourceSql = $this->getTabSql($this->sourceStruct, $tab, true);
  248. $diffs = $this->compareSql($sourceSql, $destSql);
  249. if ($diffs===false)
  250. {
  251. trigger_error('[WARNING] error parsing definition of table "'.$tab.'" - skipped');
  252. continue;
  253. }
  254. elseif (!empty($diffs))//not empty array
  255. {
  256. $info['differs'] = $diffs;
  257. }
  258. else continue;//empty array
  259. }
  260. $result[$tab] = $info;
  261. }
  262. return $result;
  263. }
  264. /**
  265. * Retrieves list of table names from the database structure dump
  266. * @access private
  267. * @param string $struct database structure listing
  268. */
  269. function getTableList($struct)
  270. {
  271. $result = array();
  272. if (preg_match_all('/CREATE(?:\s*TEMPORARY)?\s*TABLE\s*(?:IF NOT EXISTS\s*)?(?:`?(\w+)`?\.)?`?(\w+)`?/i', $struct, $m))
  273. {
  274. foreach($m[2] as $match)//m[1] is a database name if any
  275. {
  276. $result[] = $match;
  277. }
  278. }
  279. return $result;
  280. }
  281. /**
  282. * Retrieves table structure definition from the database structure dump
  283. * @access private
  284. * @param string $struct database structure listing
  285. * @param string $tab table name
  286. * @param bool $removeDatabase - either to remove database name in "CREATE TABLE database.tab"-like declarations
  287. * @return string table structure definition
  288. */
  289. function getTabSql($struct, $tab, $removeDatabase=true)
  290. {
  291. $result = '';
  292. /* create table should be single line in this case*/
  293. //1 - part before database, 2-database name, 3 - part after database
  294. if (preg_match('/(CREATE(?:\s*TEMPORARY)?\s*TABLE\s*(?:IF NOT EXISTS\s*)?)(?:`?(\w+)`?\.)?(`?('.$tab.')`?(\W|$))/i', $struct, $m, PREG_OFFSET_CAPTURE))
  295. {
  296. $tableDef = $m[0][0];
  297. $start = $m[0][1];
  298. $database = $m[2][0];
  299. $offset = $start+strlen($m[0][0]);
  300. $end = $this->getDelimPos($struct, $offset);
  301. if ($end === false)
  302. {
  303. $result = substr($struct, $start);
  304. }
  305. else
  306. {
  307. $result = substr($struct, $start, $end-$start);//already without ';'
  308. }
  309. }
  310. $result = trim($result);
  311. if ($database && $removeDatabase)
  312. {
  313. $result = str_replace($tableDef, $m[1][0].$m[3][0], $result);
  314. }
  315. return $result;
  316. }
  317. /**
  318. * Splits table sql into indexed array
  319. *
  320. */
  321. function splitTabSql($sql)
  322. {
  323. $result = array();
  324. //find opening bracket, get the prefix along with it
  325. $openBracketPos = $this->getDelimPos($sql, 0, '(');
  326. if ($openBracketPos===false)
  327. {
  328. trigger_error('[WARNING] can not find opening bracket in table definition');
  329. return false;
  330. }
  331. $prefix = substr($sql, 0, $openBracketPos+1);//prefix can not be empty, so do not check it, just trim
  332. $result[] = trim($prefix);
  333. $body = substr($sql, strlen($prefix));//fields, indexes and part after closing bracket
  334. //split by commas, get part by part
  335. while(($commaPos = $this->getDelimPos($body, 0, ',', true))!==false)
  336. {
  337. $part = trim(substr($body, 0, $commaPos+1));//read another part and shorten $body
  338. if ($part)
  339. {
  340. $result[] = $part;
  341. }
  342. $body = substr($body, $commaPos+1);
  343. }
  344. //here we have last field (or index) definition + part after closing bracket (ENGINE, ect)
  345. $closeBracketPos = $this->getDelimRpos($body, 0, ')');
  346. if ($closeBracketPos===false)
  347. {
  348. trigger_error('[WARNING] can not find closing bracket in table definition');
  349. return false;
  350. }
  351. //get last field / index definition before closing bracket
  352. $part = substr($body, 0, $closeBracketPos);
  353. $result[] = trim($part);
  354. //get the suffix part along with the closing bracket
  355. $suffix = substr($body, $closeBracketPos);
  356. $suffix = trim($suffix);
  357. if ($suffix)
  358. {
  359. $result[] = $suffix;
  360. }
  361. return $result;
  362. }
  363. /**
  364. * returns array of fields or keys definitions that differs in the given tables structure
  365. * @access private
  366. * @param sring $sourceSql table structure
  367. * @param sring $destSql right table structure
  368. * supports some $options
  369. * @return array
  370. * - [0]
  371. * - source (string) out-of-date table field definition
  372. * - dest (string) reference table field definition
  373. * - [1]...
  374. */
  375. function compareSql($sourceSql, $destSql)//$sourceSql, $destSql
  376. {
  377. $result = array();
  378. //split with comma delimiter, not line breaks
  379. $sourceParts = $this->splitTabSql($sourceSql);
  380. if ($sourceParts===false)//error parsing sql
  381. {
  382. trigger_error('[WARNING] error parsing source sql');
  383. return false;
  384. }
  385. $destParts = $this->splitTabSql($destSql);
  386. if ($destParts===false)
  387. {
  388. trigger_error('[WARNING] error parsing destination sql');
  389. return false;
  390. }
  391. $sourcePartsIndexed = array();
  392. $destPartsIndexed = array();
  393. foreach($sourceParts as $line)
  394. {
  395. $lineInfo = $this->processLine($line);
  396. if (!$lineInfo) continue;
  397. $sourcePartsIndexed[$lineInfo['key']] = $lineInfo['line'];
  398. }
  399. foreach($destParts as $line)
  400. {
  401. $lineInfo = $this->processLine($line);
  402. if (!$lineInfo) continue;
  403. $destPartsIndexed[$lineInfo['key']] = $lineInfo['line'];
  404. }
  405. $sourceKeys = array_keys($sourcePartsIndexed);
  406. $destKeys = array_keys($destPartsIndexed);
  407. $all = array_unique(array_merge($sourceKeys, $destKeys));
  408. sort($all);//fields first, then indexes - because fields are prefixed with '!'
  409. foreach ($all as $key)
  410. {
  411. $info = array('source'=>'', 'dest'=>'');
  412. $inSource= in_array($key, $sourceKeys);
  413. $inDest= in_array($key, $destKeys);
  414. $sourceOrphan = $inSource && !$inDest;
  415. $destOrphan = $inDest && !$inSource;
  416. $different = $inSource && $inDest &&
  417. strcasecmp($this->normalizeString($destPartsIndexed[$key]), $this->normalizeString($sourcePartsIndexed[$key]));
  418. if ($sourceOrphan)
  419. {
  420. $info['source'] = $sourcePartsIndexed[$key];
  421. }
  422. elseif ($destOrphan)
  423. {
  424. $info['dest'] = $destPartsIndexed[$key];
  425. }
  426. elseif ($different)
  427. {
  428. $info['source'] = $sourcePartsIndexed[$key];
  429. $info['dest'] = $destPartsIndexed[$key];
  430. }
  431. else continue;
  432. $result[] = $info;
  433. }
  434. return $result;
  435. }
  436. /**
  437. * Transforms table structure defnition line into key=>value pair where the key is a string that uniquely
  438. * defines field or key desribed
  439. * @access private
  440. * @param string $line field definition string
  441. * @return array array with single key=>value pair as described in the description
  442. * implements some options
  443. */
  444. function processLine($line)
  445. {
  446. $options = $this->config;
  447. $result = array('key'=>'', 'line'=>'');
  448. $line = rtrim(trim($line), ',');
  449. if (preg_match('/^(CREATE\s+TABLE)|(\) ENGINE=)/i', $line))//first or last table definition line
  450. {
  451. return false;
  452. }
  453. //if (preg_match('/^(PRIMARY KEY)|(((UNIQUE )|(FULLTEXT ))?KEY `?\w+`?)/i', $line, $m))//key definition
  454. if (preg_match('/^(PRIMARY\s+KEY)|(((UNIQUE\s+)|(FULLTEXT\s+))?KEY\s+`?\w+`?)/i', $line, $m))//key definition
  455. {
  456. $key = $m[0];
  457. }
  458. elseif (preg_match('/^`?\w+`?/i', $line, $m))//field definition
  459. {
  460. $key = '!'.$m[0];//to make sure fields will be synchronised before the keys
  461. }
  462. else
  463. {
  464. return false;//line has no valuable info (empty or comment)
  465. }
  466. //$key = str_replace('`', '', $key);
  467. if (!empty($options['varcharDefaultIgnore']))
  468. {
  469. $line = preg_replace("/(var)?char\(([0-9]+)\)\s+NOT\s+NULL\s+default\s+''/i", '$1char($2) NOT NULL', $line);
  470. }
  471. if (!empty($options['intDefaultIgnore']))
  472. {
  473. $line = preg_replace("/((?:big)|(?:tiny))?int\(([0-9]+)\)\s+NOT\s+NULL\s+default\s+'0'/i", '$1int($2) NOT NULL', $line);
  474. }
  475. if (!empty($options['ignoreIncrement']))
  476. {
  477. $line = preg_replace("/ AUTO_INCREMENT=[0-9]+/i", '', $line);
  478. }
  479. $result['key'] = $this->normalizeString($key);
  480. $result['line']= $line;
  481. return $result;
  482. }
  483. /**
  484. * Takes an output of compare() method to generate the set of sql needed to update source table to make it
  485. * look as a destination one
  486. * @access private
  487. * @param array $diff compare() method output
  488. * @return array list of sql statements
  489. * supports query generation options
  490. */
  491. function getDiffSql($diff)//maybe add option to ommit or force 'IF NOT EXISTS', skip autoincrement
  492. {
  493. $options = $this->config;
  494. $sqls = array();
  495. if (!is_array($diff) || empty($diff))
  496. {
  497. return $sqls;
  498. }
  499. foreach($diff as $tab=>$info)
  500. {
  501. if ($info['sourceOrphan'])//delete it
  502. {
  503. $sqls[] = 'DROP TABLE `'.$tab.'`';
  504. }
  505. elseif ($info['destOrphan'])//create destination table in source
  506. {
  507. $database = '';
  508. $destSql = $this->getTabSql($this->destStruct, $tab, $database);
  509. if (!empty($options['ignoreIncrement']))
  510. {
  511. $destSql = preg_replace("/\s*AUTO_INCREMENT=[0-9]+/i", '', $destSql);
  512. }
  513. if (!empty($options['ingoreIfNotExists']))
  514. {
  515. $destSql = preg_replace("/IF NOT EXISTS\s*/i", '', $destSql);
  516. }
  517. if (!empty($options['forceIfNotExists']))
  518. {
  519. $destSql = preg_replace('/(CREATE(?:\s*TEMPORARY)?\s*TABLE\s*)(?:IF\sNOT\sEXISTS\s*)?(`?\w+`?)/i', '$1IF NOT EXISTS $2', $destSql);
  520. }
  521. $sqls[] = $destSql;
  522. }
  523. else
  524. {
  525. foreach($info['differs'] as $finfo)
  526. {
  527. $inDest = !empty($finfo['dest']);
  528. $inSource = !empty($finfo['source']);
  529. if ($inSource && !$inDest)
  530. {
  531. $sql = $finfo['source'];
  532. $action = 'drop';
  533. }
  534. elseif ($inDest && !$inSource)
  535. {
  536. $sql = $finfo['dest'];
  537. $action = 'add';
  538. }
  539. else
  540. {
  541. $sql = $finfo['dest'];
  542. $action = 'modify';
  543. }
  544. $sql = $this->getActionSql($action, $tab, $sql);
  545. $sqls[] = $sql;
  546. }
  547. }
  548. }
  549. return $sqls;
  550. }
  551. /**
  552. * Compiles update sql
  553. * @access private
  554. * @param string $action - 'drop', 'add' or 'modify'
  555. * @param string $tab table name
  556. * @param string $sql definition of the element to change
  557. * @return string update sql
  558. */
  559. function getActionSql($action, $tab, $sql)
  560. {
  561. $result = 'ALTER TABLE `'.$tab.'` ';
  562. $action = strtolower($action);
  563. $keyField = '`?\w`?(?:\(\d+\))?';//matches `name`(10)
  564. $keyFieldList = '(?:'.$keyField.'(?:,\s?)?)+';//matches `name`(10),`desc`(255)
  565. if (preg_match('/((?:PRIMARY )|(?:UNIQUE )|(?:FULLTEXT ))?KEY `?(\w+)?`?\s(\('.$keyFieldList.'\))/i', $sql, $m))
  566. { //key and index operations
  567. $type = strtolower(trim($m[1]));
  568. $name = trim($m[2]);
  569. $fields = trim($m[3]);
  570. switch($action)
  571. {
  572. case 'drop':
  573. if ($type=='primary')
  574. {
  575. $result.= 'DROP PRIMARY KEY';
  576. }
  577. else
  578. {
  579. $result.= 'DROP INDEX `'.$name.'`';
  580. }
  581. break;
  582. case 'add':
  583. if ($type=='primary')
  584. {
  585. $result.= 'ADD PRIMARY KEY '.$fields;
  586. }
  587. elseif ($type=='')
  588. {
  589. $result.= 'ADD INDEX `'.$name.'` '.$fields;
  590. }
  591. else
  592. {
  593. $result .='ADD '.strtoupper($type).' `'.$name.'` '.$fields;//fulltext or unique
  594. }
  595. break;
  596. case 'modify':
  597. if ($type=='primary')
  598. {
  599. $result.='DROP PRIMARY KEY, ADD PRIMARY KEY '.$fields;
  600. }
  601. elseif ($type=='')
  602. {
  603. $result.='DROP INDEX `'.$name.'`, ADD INDEX `'.$name.'` '.$fields;
  604. }
  605. else
  606. {
  607. $result.='DROP INDEX `'.$name.'`, ADD '.strtoupper($type).' `'.$name.'` '.$fields;//fulltext or unique
  608. }
  609. break;
  610. }
  611. }
  612. else //fields operations
  613. {
  614. $sql = rtrim(trim($sql), ',');
  615. $result.= strtoupper($action);
  616. if ($action=='drop')
  617. {
  618. $spacePos = strpos($sql, ' ');
  619. $result.= ' '.substr($sql, 0, $spacePos);
  620. }
  621. else
  622. {
  623. $result.= ' '.$sql;
  624. }
  625. }
  626. return $result;
  627. }
  628. /**
  629. * Searches for the position of the next delimiter which is not inside string literal like 'this ; ' or
  630. * like "this ; ".
  631. *
  632. * Handles escaped \" and \'. Also handles sql comments.
  633. * Actualy it is regex-based Finit State Machine (FSN)
  634. */
  635. function getDelimPos($string, $offset=0, $delim=';', $skipInBrackets=false)
  636. {
  637. $stack = array();
  638. $rbs = '\\\\'; //reg - escaped backslash
  639. $regPrefix = "(?<!$rbs)(?:$rbs{2})*";
  640. $reg = $regPrefix.'("|\')|(/\\*)|(\\*/)|(-- )|(\r\n|\r|\n)|';
  641. if ($skipInBrackets)
  642. {
  643. $reg.='(\(|\))|';
  644. }
  645. else
  646. {
  647. $reg.='()';
  648. }
  649. $reg .= '('.preg_quote($delim).')';
  650. while (preg_match('%'.$reg.'%', $string, $m, PREG_OFFSET_CAPTURE, $offset))
  651. {
  652. $offset = $m[0][1]+strlen($m[0][0]);
  653. if (end($stack)=='/*')
  654. {
  655. if (!empty($m[3][0]))
  656. {
  657. array_pop($stack);
  658. }
  659. continue;//here we could also simplify regexp
  660. }
  661. if (end($stack)=='-- ')
  662. {
  663. if (!empty($m[5][0]))
  664. {
  665. array_pop($stack);
  666. }
  667. continue;//here we could also simplify regexp
  668. }
  669. if (!empty($m[7][0]))// ';' found
  670. {
  671. if (empty($stack))
  672. {
  673. return $m[7][1];
  674. }
  675. else
  676. {
  677. //var_dump($stack, substr($string, $offset-strlen($m[0][0])));
  678. }
  679. }
  680. if (!empty($m[6][0]))// '(' or ')' found
  681. {
  682. if (empty($stack) && $m[6][0]=='(')
  683. {
  684. array_push($stack, $m[6][0]);
  685. }
  686. elseif($m[6][0]==')' && end($stack)=='(')
  687. {
  688. array_pop($stack);
  689. }
  690. }
  691. elseif (!empty($m[1][0]))// ' or " found
  692. {
  693. if (end($stack)==$m[1][0])
  694. {
  695. array_pop($stack);
  696. }
  697. else
  698. {
  699. array_push($stack, $m[1][0]);
  700. }
  701. }
  702. elseif (!empty($m[2][0])) // opening comment / *
  703. {
  704. array_push($stack, $m[2][0]);
  705. }
  706. elseif (!empty($m[4][0])) // opening comment --
  707. {
  708. array_push($stack, $m[4][0]);
  709. }
  710. }
  711. return false;
  712. }
  713. /**
  714. * works the same as getDelimPos except returns position of the first occurence of the delimiter starting from
  715. * the end of the string
  716. */
  717. function getDelimRpos($string, $offset=0, $delim=';', $skipInBrackets=false)
  718. {
  719. $pos = $this->getDelimPos($string, $offset, $delim, $skipInBrackets);
  720. if ($pos===false)
  721. {
  722. return false;
  723. }
  724. do
  725. {
  726. $newPos=$this->getDelimPos($string, $pos+1, $delim, $skipInBrackets);
  727. if ($newPos !== false)
  728. {
  729. $pos = $newPos;
  730. }
  731. }
  732. while($newPos!==false);
  733. return $pos;
  734. }
  735. /**
  736. * Converts string to lowercase and replaces repeated spaces with the single one -
  737. * to be used for the comparison purposes only
  738. * @param string $str string to normaize
  739. */
  740. function normalizeString($str)
  741. {
  742. $str = strtolower($str);
  743. $str = preg_replace('/\s+/', ' ', $str);
  744. return $str;
  745. }
  746. }