CRM-14995 tidyups
[civicrm-core.git] / CRM / Core / I18n / Schema.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
06b69b18 4 | CiviCRM version 4.5 |
6a488035 5 +--------------------------------------------------------------------+
06b69b18 6 | Copyright CiviCRM LLC (c) 2004-2014 |
6a488035
TO
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26*/
27
28/**
29 *
30 * @package CRM
06b69b18 31 * @copyright CiviCRM LLC (c) 2004-2014
6a488035
TO
32 * $Id$
33 *
34 */
35class CRM_Core_I18n_Schema {
36
37 /**
38 * Drop all views (for use by CRM_Core_DAO::dropAllTables() mostly).
39 *
40 * @return void
41 */
42 static function dropAllViews() {
43 $domain = new CRM_Core_DAO_Domain();
44 $domain->find(TRUE);
45 if (!$domain->locales) {
46 return;
47 }
48
49 $locales = explode(CRM_Core_DAO::VALUE_SEPARATOR, $domain->locales);
50 $tables = CRM_Core_I18n_SchemaStructure::tables();
51
52 foreach ($locales as $locale) {
53 foreach ($tables as $table) {
54 CRM_Core_DAO::executeQuery("DROP VIEW IF EXISTS {$table}_{$locale}");
55 }
56 }
57 }
58
59 /**
60 * Switch database from single-lang to multi (by adding
61 * the first language and dropping the original columns).
62 *
63 * @param $locale string the first locale to create (migrate to)
64 *
65 * @return void
66 */
67 static function makeMultilingual($locale) {
68 $domain = new CRM_Core_DAO_Domain();
69 $domain->find(TRUE);
70
71 // break early if the db is already multi-lang
72 if ($domain->locales) {
73 return;
74 }
75
76 $dao = new CRM_Core_DAO();
77
78 // build the column-adding SQL queries
79 $columns = CRM_Core_I18n_SchemaStructure::columns();
80 $indices = CRM_Core_I18n_SchemaStructure::indices();
81 $queries = array();
82 foreach ($columns as $table => $hash) {
83 // drop old indices
84 if (isset($indices[$table])) {
85 foreach ($indices[$table] as $index) {
86 $queries[] = "DROP INDEX {$index['name']} ON {$table}";
87 }
88 }
89 // deal with columns
90 foreach ($hash as $column => $type) {
91 $queries[] = "ALTER TABLE {$table} ADD {$column}_{$locale} {$type}";
92 $queries[] = "UPDATE {$table} SET {$column}_{$locale} = {$column}";
93 $queries[] = "ALTER TABLE {$table} DROP {$column}";
94 }
95
96 // add view
97 $queries[] = self::createViewQuery($locale, $table, $dao);
98
99 // add new indices
100 $queries = array_merge($queries, array_values(self::createIndexQueries($locale, $table)));
101 }
102
103 // execute the queries without i18n rewriting
104 foreach ($queries as $query) {
105 $dao->query($query, FALSE);
106 }
107
108 // update civicrm_domain.locales
109 $domain->locales = $locale;
110 $domain->save();
111 }
112
113 /**
114 * Switch database from multi-lang back to single (by dropping
115 * additional columns and views and retaining only the selected locale).
116 *
117 * @param $retain string the locale to retain
118 *
119 * @return void
120 */
121 static function makeSinglelingual($retain) {
122 $domain = new CRM_Core_DAO_Domain;
123 $domain->find(TRUE);
124 $locales = explode(CRM_Core_DAO::VALUE_SEPARATOR, $domain->locales);
125
126 // break early if the db is already single-lang
127 if (!$locales) {
128 return;
129 }
130
131 // lets drop all triggers first
132 $logging = new CRM_Logging_Schema;
133 $logging->dropTriggers( );
134
135 // turn subsequent tables singlelingual
136 $tables = CRM_Core_I18n_SchemaStructure::tables();
137 foreach ($tables as $table) {
138 self::makeSinglelingualTable($retain, $table);
139 }
140
141 // update civicrm_domain.locales
142 $domain->locales = 'NULL';
143 $domain->save();
144
145 //CRM-6963 -fair assumption.
146 global $dbLocale;
147 $dbLocale = '';
148
149 // now lets rebuild all triggers
150 CRM_Core_DAO::triggerRebuild( );
151 }
152
153 /**
154 * Switch a given table from multi-lang to single (by retaining only the selected locale).
155 *
156 * @param $retain string the locale to retain
157 * @param $table string the table containing the column
158 * @param $class string schema structure class to use to recreate indices
159 *
da6b46f4
EM
160 * @param array $triggers
161 *
6a488035
TO
162 * @return void
163 */
164 static function makeSinglelingualTable(
165 $retain,
166 $table,
167 $class = 'CRM_Core_I18n_SchemaStructure',
168 $triggers = array()
169 ) {
170 $domain = new CRM_Core_DAO_Domain;
171 $domain->find(TRUE);
172 $locales = explode(CRM_Core_DAO::VALUE_SEPARATOR, $domain->locales);
173
174 // break early if the db is already single-lang
175 if (!$locales) {
176 return;
177 }
178
0e6e8724
DL
179 $columns =& $class::columns();
180 $indices =& $class::indices();
6a488035
TO
181 $queries = array();
182 $dropQueries = array();
183 // drop indices
184 if (isset($indices[$table])) {
185 foreach ($indices[$table] as $index) {
186 foreach ($locales as $loc) {
187 $queries[] = "DROP INDEX {$index['name']}_{$loc} ON {$table}";
188 }
189 }
190 }
191
192 // deal with columns
193 foreach ($columns[$table] as $column => $type) {
194 $queries[] = "ALTER TABLE {$table} ADD {$column} {$type}";
195 $queries[] = "UPDATE {$table} SET {$column} = {$column}_{$retain}";
196 foreach ($locales as $loc) {
197 $dropQueries[] = "ALTER TABLE {$table} DROP {$column}_{$loc}";
198 }
199 }
200
201 // drop views
202 foreach ($locales as $loc) {
203 $queries[] = "DROP VIEW {$table}_{$loc}";
204 }
205
206 // add original indices
207 $queries = array_merge($queries, self::createIndexQueries(NULL, $table));
208
209 // execute the queries without i18n rewriting
210 $dao = new CRM_Core_DAO;
211 foreach ($queries as $query) {
212 $dao->query($query, FALSE);
213 }
214
215 foreach ($dropQueries as $query) {
216 $dao->query($query, FALSE);
217 }
218
219 if ( !empty($triggers)) {
220 if (CRM_Core_Config::isUpgradeMode()) {
221 foreach ($triggers as $triggerInfo) {
222 $when = $triggerInfo['when'];
223 $event = $triggerInfo['event'];
224 $triggerName = "{$table}_{$when}_{$event}";
225 CRM_Core_DAO::executeQuery("DROP TRIGGER IF EXISTS {$triggerName}");
226 }
227 }
228
229 // invoke the meta trigger creation call
230 CRM_Core_DAO::triggerRebuild($table);
231 }
232 }
233
234 /**
235 * Add a new locale to a multi-lang db, setting
236 * its values to the current default locale.
237 *
238 * @param $locale string the new locale to add
239 * @param $source string the locale to copy from
240 *
241 * @return void
242 */
243 static function addLocale($locale, $source) {
244 // get the current supported locales
245 $domain = new CRM_Core_DAO_Domain();
246 $domain->find(TRUE);
247 $locales = explode(CRM_Core_DAO::VALUE_SEPARATOR, $domain->locales);
248
249 // break early if the locale is already supported
250 if (in_array($locale, $locales)) {
251 return;
252 }
253
254 $dao = new CRM_Core_DAO();
255
256 // build the required SQL queries
257 $columns = CRM_Core_I18n_SchemaStructure::columns();
258 $indices = CRM_Core_I18n_SchemaStructure::indices();
259 $queries = array();
260 foreach ($columns as $table => $hash) {
261 // add new columns
262 foreach ($hash as $column => $type) {
263 // CRM-7854: skip existing columns
264 if (CRM_Core_DAO::checkFieldExists($table, "{$column}_{$locale}", FALSE)) {
265 continue;
266 }
267 $queries[] = "ALTER TABLE {$table} ADD {$column}_{$locale} {$type}";
268 $queries[] = "UPDATE {$table} SET {$column}_{$locale} = {$column}_{$source}";
269 }
270
271 // add view
272 $queries[] = self::createViewQuery($locale, $table, $dao);
273
274 // add new indices
275 $queries = array_merge($queries, array_values(self::createIndexQueries($locale, $table)));
276 }
277
278 // execute the queries without i18n rewriting
279 foreach ($queries as $query) {
280 $dao->query($query, FALSE);
281 }
282
283 // update civicrm_domain.locales
284 $locales[] = $locale;
285 $domain->locales = implode(CRM_Core_DAO::VALUE_SEPARATOR, $locales);
286 $domain->save();
287
288 // invoke the meta trigger creation call
289 CRM_Core_DAO::triggerRebuild();
290 }
291
292 /**
293 * Rebuild multilingual indices, views and triggers (useful for upgrades)
294 *
295 * @param $locales array locales to be rebuilt
296 * @param $version string version of schema structure to use
297 *
298 * @return void
299 */
300 static function rebuildMultilingualSchema($locales, $version = NULL) {
301 if ($version) {
302 $latest = self::getLatestSchema($version);
303 require_once "CRM/Core/I18n/SchemaStructure_{$latest}.php";
304 $class = "CRM_Core_I18n_SchemaStructure_{$latest}";
305 }
306 else {
307 $class = 'CRM_Core_I18n_SchemaStructure';
308 }
0e6e8724
DL
309 $indices =& $class::indices();
310 $tables =& $class::tables();
6a488035
TO
311 $queries = array();
312 $dao = new CRM_Core_DAO;
313
314 // get all of the already existing indices
315 $existing = array();
316 foreach (array_keys($indices) as $table) {
317 $existing[$table] = array();
318 $dao->query("SHOW INDEX FROM $table", FALSE);
319 while ($dao->fetch()) {
320 if (preg_match('/_[a-z][a-z]_[A-Z][A-Z]$/', $dao->Key_name)) {
321 $existing[$table][] = $dao->Key_name;
322 }
323 }
324 }
325
326 // from all of the CREATE INDEX queries fetch the ones creating missing indices
327 foreach ($locales as $locale) {
328 foreach (array_keys($indices) as $table) {
329 $allQueries = self::createIndexQueries($locale, $table, $class);
330 foreach ($allQueries as $name => $query) {
331 if (!in_array("{$name}_{$locale}", $existing[$table])) {
332 $queries[] = $query;
333 }
334 }
335 }
336 }
337
338 // rebuild views
339 foreach ($locales as $locale) {
340 foreach ($tables as $table) {
341 $queries[] = self::createViewQuery($locale, $table, $dao, $class);
342 }
343 }
344
345 // rebuild triggers
346 $last = array_pop($locales);
347
348 foreach ($queries as $query) {
349 $dao->query($query, FALSE);
350 }
351
352 // invoke the meta trigger creation call
353 CRM_Core_DAO::triggerRebuild();
354 }
355
356 /**
357 * Rewrite SQL query to use views to access tables with localized columns.
358 *
359 * @param $query string the query for rewrite
360 *
361 * @return string the rewritten query
362 */
363 static function rewriteQuery($query) {
364 global $dbLocale;
365 $tables = self::schemaStructureTables();
366 foreach ($tables as $table) {
367 $query = preg_replace("/([^'\"])({$table})([^_'\"])/", "\\1\\2{$dbLocale}\\3", $query);
368 }
369 // uncomment the below to rewrite the civicrm_value_* queries
370 // $query = preg_replace("/(civicrm_value_[a-z0-9_]+_\d+)([^_])/", "\\1{$dbLocale}\\2", $query);
371 return $query;
372 }
373
a0ee3941
EM
374 /**
375 * @param null $version
376 * @param bool $force
377 *
378 * @return array
379 */
6a488035
TO
380 static function schemaStructureTables($version = NULL, $force = FALSE) {
381 static $_tables = NULL;
382 if ($_tables === NULL || $force) {
383 if ($version) {
384 $latest = self::getLatestSchema($version);
385 // FIXME: Doing require_once is a must here because a call like CRM_Core_I18n_SchemaStructure_4_1_0 makes
386 // class loader look for file like - CRM/Core/I18n/SchemaStructure/4/1/0.php which is not what we want to be loaded
387 require_once "CRM/Core/I18n/SchemaStructure_{$latest}.php";
388 $class = "CRM_Core_I18n_SchemaStructure_{$latest}";
0e6e8724 389 $tables =& $class::tables();
6a488035
TO
390 }
391 else {
392 $tables = CRM_Core_I18n_SchemaStructure::tables();
393 }
394 $_tables = $tables;
395 }
396 return $_tables;
397 }
398
a0ee3941
EM
399 /**
400 * @param $version
401 *
402 * @return mixed
403 */
6a488035
TO
404 static function getLatestSchema($version) {
405 // remove any .upgrade sub-str from version. Makes it easy to do version_compare & give right result
406 $version = str_ireplace(".upgrade", "", $version);
407
408 // fetch all the SchemaStructure versions we ship and sort by version
409 $schemas = array();
410 foreach (scandir(dirname(__FILE__)) as $file) {
411 $matches = array();
412 if (preg_match('/^SchemaStructure_([0-9a-z_]+)\.php$/', $file, $matches)) {
413 $schemas[] = str_replace('_', '.', $matches[1]);
414 }
415 }
416 usort($schemas, 'version_compare');
417
418 // find the latest schema structure older than (or equal to) $version
419 do {
420 $latest = array_pop($schemas);
421 } while (version_compare($latest, $version, '>'));
422
423 return str_replace('.', '_', $latest);
424 }
425
426 /**
427 * CREATE INDEX queries for a given locale and table
428 *
429 * @param $locale string locale for which the queries should be created (null to create original indices)
430 * @param $table string table for which the queries should be created
431 * @param $class string schema structure class to use
432 *
433 * @return array array of CREATE INDEX queries
434 */
435 private static function createIndexQueries($locale, $table, $class = 'CRM_Core_I18n_SchemaStructure') {
0e6e8724
DL
436 $indices =& $class::indices();
437 $columns =& $class::columns();
6a488035
TO
438 if (!isset($indices[$table])) {
439 return array();
440 }
441
442 $queries = array();
443 foreach ($indices[$table] as $index) {
444 $unique = isset($index['unique']) && $index['unique'] ? 'UNIQUE' : '';
445 foreach ($index['field'] as $i => $col) {
446 // if a given column is localizable, extend its name with the locale
447 if ($locale and isset($columns[$table][$col])) {
448 $index['field'][$i] = "{$col}_{$locale}";
449 }
450 }
451 $cols = implode(', ', $index['field']);
452 $name = $index['name'];
453 if ($locale) {
454 $name .= '_' . $locale;
455 }
456 // CRM-7854: skip existing indices
457 if (CRM_Core_DAO::checkConstraintExists($table, $name)) {
458 continue;
459 }
460 $queries[$index['name']] = "CREATE {$unique} INDEX {$name} ON {$table} ({$cols})";
461 }
462 return $queries;
463 }
464
465 /**
466 * CREATE VIEW query for a given locale and table
467 *
468 * @param $locale string locale of the view
469 * @param $table string table of the view
470 * @param $dao object a DAO object to run DESCRIBE queries
471 * @param $class string schema structure class to use
472 *
473 * @return array array of CREATE INDEX queries
474 */
475 private static function createViewQuery($locale, $table, &$dao, $class = 'CRM_Core_I18n_SchemaStructure') {
0e6e8724 476 $columns =& $class::columns();
6a488035
TO
477 $cols = array();
478 $dao->query("DESCRIBE {$table}", FALSE);
479 while ($dao->fetch()) {
480 // view non-internationalized columns directly
481 if (!in_array($dao->Field, array_keys($columns[$table])) and
482 !preg_match('/_[a-z][a-z]_[A-Z][A-Z]$/', $dao->Field)
483 ) {
484 $cols[] = $dao->Field;
485 }
486 }
487 // view intrernationalized columns through an alias
488 foreach ($columns[$table] as $column => $_) {
489 $cols[] = "{$column}_{$locale} {$column}";
490 }
491 return "CREATE OR REPLACE VIEW {$table}_{$locale} AS SELECT " . implode(', ', $cols) . " FROM {$table}";
492 }
493
a0ee3941
EM
494 /**
495 * @param $info
496 * @param null $tableName
497 */
6a488035
TO
498 static function triggerInfo(&$info, $tableName = NULL) {
499 // get the current supported locales
500 $domain = new CRM_Core_DAO_Domain();
501 $domain->find(TRUE);
502 if (empty($domain->locales)) {
503 return;
504 }
505
506 $locales = explode(CRM_Core_DAO::VALUE_SEPARATOR, $domain->locales);
507 $locale = array_pop($locales);
508
509 // CRM-10027
510 if (count($locales) == 0) {
511 return;
512 }
513
514 $currentVer = CRM_Core_BAO_Domain::version(TRUE);
515
516 if ($currentVer && CRM_Core_Config::isUpgradeMode()) {
517 // take exact version so that proper schema structure file in invoked
518 $latest = self::getLatestSchema($currentVer);
519 require_once "CRM/Core/I18n/SchemaStructure_{$latest}.php";
520 $class = "CRM_Core_I18n_SchemaStructure_{$latest}";
521 }
522 else {
523 $class = 'CRM_Core_I18n_SchemaStructure';
524 }
525
0e6e8724 526 $columns =& $class::columns();
6a488035
TO
527
528 foreach ($columns as $table => $hash) {
529 if ($tableName &&
530 $tableName != $table
531 ) {
532 continue;
533 }
534
535 $trigger = array();
536
537 foreach ($hash as $column => $_) {
538 $trigger[] = "IF NEW.{$column}_{$locale} IS NOT NULL THEN";
539 foreach ($locales as $old) {
540 $trigger[] = "IF NEW.{$column}_{$old} IS NULL THEN SET NEW.{$column}_{$old} = NEW.{$column}_{$locale}; END IF;";
541 }
542 foreach ($locales as $old) {
543 $trigger[] = "ELSEIF NEW.{$column}_{$old} IS NOT NULL THEN";
544 foreach (array_merge($locales, array(
545 $locale)) as $loc) {
546 if ($loc == $old) {
547 continue;
548 }
549 $trigger[] = "IF NEW.{$column}_{$loc} IS NULL THEN SET NEW.{$column}_{$loc} = NEW.{$column}_{$old}; END IF;";
550 }
551 }
552 $trigger[] = 'END IF;';
553 }
554
555 $sql = implode(' ', $trigger);
556 $info[] = array('table' => array($table),
557 'when' => 'BEFORE',
558 'event' => array('UPDATE'),
559 'sql' => $sql,
560 );
561 }
562
563 // take care of the ON INSERT triggers
564 foreach ($columns as $table => $hash) {
565 $trigger = array();
566 foreach ($hash as $column => $_) {
567 $trigger[] = "IF NEW.{$column}_{$locale} IS NOT NULL THEN";
568 foreach ($locales as $old) {
569 $trigger[] = "SET NEW.{$column}_{$old} = NEW.{$column}_{$locale};";
570 }
571 foreach ($locales as $old) {
572 $trigger[] = "ELSEIF NEW.{$column}_{$old} IS NOT NULL THEN";
573 foreach (array_merge($locales, array(
574 $locale)) as $loc) {
575 if ($loc == $old) {
576 continue;
577 }
578 $trigger[] = "SET NEW.{$column}_{$loc} = NEW.{$column}_{$old};";
579 }
580 }
581 $trigger[] = 'END IF;';
582 }
583
584 $sql = implode(' ', $trigger);
585 $info[] = array('table' => array($table),
586 'when' => 'BEFORE',
587 'event' => array('INSERT'),
588 'sql' => $sql,
589 );
590 }
591 }
592}
593