1<?php 2/* 3 Copyright (C) 2008-2014 Hewlett-Packard Development Company, L.P. 4 Copyright (C) 2014-2015, 2018 Siemens AG 5 6 This library is free software; you can redistribute it and/or 7 modify it under the terms of the GNU Lesser General Public 8 License version 2.1 as published by the Free Software Foundation. 9 10 This library is distributed in the hope that it will be useful, 11 but WITHOUT ANY WARRANTY; without even the implied warranty of 12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 Lesser General Public License for more details. 14 15 You should have received a copy of the GNU Lesser General Public License 16 along with this library; if not, write to the Free Software Foundation, Inc.0 17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 18 */ 19 20/** 21 * @file 22 * @brief Functions to bring database schema to a known state. 23 * 24 **/ 25 26require_once(__DIR__ . '/../../vendor/autoload.php'); 27 28use Fossology\Lib\Db\DbManager; 29use Fossology\Lib\Db\Driver; 30use Fossology\Lib\Db\Driver\Postgres; 31use Fossology\Lib\Db\ModernDbManager; 32use Monolog\Handler\ErrorLogHandler; 33use Monolog\Logger; 34 35/** 36 * @class fo_libschema 37 * @brief Class to handle database schema 38 */ 39class fo_libschema 40{ 41 /** 42 * @var bool $debug 43 * Set true to enable debugging 44 */ 45 public $debug = false; 46 47 /** 48 * @var DbManager $dbman 49 * DB manager to use 50 */ 51 private $dbman; 52 53 /** 54 * @var array $schema 55 * Schema to be applied 56 */ 57 private $schema = array(); 58 59 /** 60 * @var array $currSchema 61 * Current schema of the DB 62 */ 63 private $currSchema = array(); 64 65 /** 66 * Constructor for fo_libschema 67 * @param DbManager $dbManager 68 */ 69 function __construct(DbManager &$dbManager) 70 { 71 $this->dbman = $dbManager; 72 } 73 74 /** 75 * Set the Driver for the DbManager 76 * @param Driver $dbDriver 77 */ 78 function setDriver(Driver &$dbDriver) 79 { 80 $this->dbman->setDriver($dbDriver); 81 } 82 83 84 /** 85 * Apply or echo the SQL statement based on the debugging status. 86 * @param string $sql Statement to be applied 87 * @param string $stmt Name of the statement (for caching) 88 * @return DB result 89 */ 90 function applyOrEchoOnce($sql, $stmt = '') 91 { 92 if ($this->debug) { 93 print ("$sql\n"); 94 } else { 95 return $this->dbman->queryOnce($sql, $stmt); 96 } 97 } 98 99 100 /** 101 * @brief Make schema match $Filename. This is a single transaction. 102 * @param string $filename Schema file written by schema-export.php 103 * @param bool $debug Turn on debugging (echo sql as it is being executed) 104 * @param string $catalog Optional database name 105 * @param array[] $migrateColumns array('tablename'=>array('col1','col2'),...) of columns which should not be deleted 106 * @return false=success, on error return string with error message. 107 **/ 108 function applySchema($filename = NULL, $debug = false, $catalog = 'fossology', $migrateColumns = array()) 109 { 110 global $PG_CONN; 111 112 // first check to make sure we don't already have the plpgsql language installed 113 $sql_statement = "select lanname from pg_language where lanname = 'plpgsql'"; 114 115 $result = pg_query($PG_CONN, $sql_statement); 116 if (!$result) { 117 throw new Exception("Could not check the database for plpgsql language"); 118 } 119 120 $plpgsql_already_installed = false; 121 if ( pg_fetch_row($result) ) { 122 $plpgsql_already_installed = true; 123 } 124 125 // then create language plpgsql if not already created 126 if ($plpgsql_already_installed == false) { 127 $sql_statement = "CREATE LANGUAGE plpgsql"; 128 $result = pg_query($PG_CONN, $sql_statement); 129 if (!$result) { 130 throw new Exception("Could not create plpgsql language in the database"); 131 } 132 } 133 134 $sql_statement = "select extname from pg_extension where extname = 'uuid-ossp'"; 135 136 $result = pg_query($PG_CONN, $sql_statement); 137 if (!$result) { 138 throw new Exception("Could not check the database for uuid-ossp extension"); 139 } 140 141 $uuid_already_installed = false; 142 if ( pg_fetch_row($result) ) { 143 $uuid_already_installed = true; 144 } 145 146 // then create extension uuid-ossp if not already created 147 if ( $uuid_already_installed == false ) { 148 $sql_statement = 'CREATE EXTENSION "uuid-ossp";'; 149 $result = pg_query($PG_CONN, $sql_statement); 150 if (!$result) { 151 throw new Exception("Could not create uuid-ossp extension in the database"); 152 } 153 } 154 155 $this->debug = $debug; 156 if (!file_exists($filename)) { 157 $errMsg = "$filename does not exist."; 158 return $errMsg; 159 } 160 $Schema = array(); /* will be filled in next line */ 161 require($filename); /* this cause Fatal Error if the file does not exist. */ 162 $this->schema = $Schema; 163 164 /* Very basic sanity check (so we don't delete everything!) */ 165 if ((count($this->schema['TABLE']) < 5) || (count($this->schema['SEQUENCE']) < 5) 166 || (count($this->schema['INDEX']) < 5) || (count($this->schema['CONSTRAINT']) < 5) 167 ) { 168 $errMsg = "Schema from '$filename' appears invalid."; 169 return $errMsg; 170 } 171 172 if (!$debug) { 173 $result = $this->dbman->getSingleRow("show statement_timeout", array(), $stmt = __METHOD__ . '.getTimeout'); 174 $statementTimeout = $result['statement_timeout']; 175 $this->dbman->queryOnce("SET statement_timeout = 0", $stmt = __METHOD__ . '.setTimeout'); 176 } 177 178 $this->applyOrEchoOnce('BEGIN'); 179 $this->getCurrSchema(); 180 $errlev = error_reporting(E_ERROR | E_WARNING | E_PARSE); 181 $this->applySequences(); 182 $this->applyTables(); 183 $this->applyInheritedRelations(); 184 $this->applyTables(true); 185 $this->updateSequences(); 186 $this->applyViews(); 187 $this->dropConstraints(); 188 /* Reload current since the CASCADE may have changed things */ 189 $this->getCurrSchema(); /* constraints and indexes are linked, recheck */ 190 $this->dropIndexes(); 191 $this->applyIndexes(); 192 $this->applyConstraints(); 193 error_reporting($errlev); /* return to previous error reporting level */ 194 $this->makeFunctions(); 195 $this->applyClusters(); 196 /* Reload current since CASCADE during migration may have changed things */ 197 $this->getCurrSchema(); 198 $this->dropViews($catalog); 199 foreach ($this->currSchema['TABLE'] as $table => $columns) { 200 $skipColumns = array_key_exists($table, $migrateColumns) ? $migrateColumns[$table] : array(); 201 $dropColumns = array_diff(array_keys($columns), $skipColumns); 202 $this->dropColumnsFromTable($dropColumns, $table); 203 } 204 $this->applyOrEchoOnce('COMMIT'); 205 flush(); 206 ReportCachePurgeAll(); 207 if (!$debug) { 208 $this->dbman->getSingleRow("SET statement_timeout = $statementTimeout", array(), $stmt = __METHOD__ . '.resetTimeout'); 209 print "DB schema has been updated for $catalog.\n"; 210 } else { 211 print "These queries could update DB schema for $catalog.\n"; 212 } 213 return false; 214 } 215 216 /** 217 * @brief Add sequences to the database 218 * 219 * The function first checks if the sequence already exists or not. 220 * The sequence is only created only if it does not exists. 221 */ 222 function applySequences() 223 { 224 if (empty($this->schema['SEQUENCE'])) { 225 return; 226 } 227 foreach ($this->schema['SEQUENCE'] as $name => $import) { 228 if (empty($name)) { 229 continue; 230 } 231 232 if (!array_key_exists('SEQUENCE', $this->currSchema) 233 || !array_key_exists($name, $this->currSchema['SEQUENCE'])) { 234 $createSql = is_string($import) ? $import : $import['CREATE']; 235 $this->applyOrEchoOnce($createSql, $stmt = __METHOD__ . "." . $name . ".CREATE"); 236 } 237 } 238 } 239 /** 240 * @brief Add clusters 241 * 242 * The function first checks if the cluster already exists or not. 243 * The cluster is only created only if it does not exists. 244 */ 245 function applyClusters() 246 { 247 if (empty($this->schema['CLUSTER'])) { 248 return; 249 } 250 foreach ($this->schema['CLUSTER'] as $name => $sql) { 251 if (empty($name)) { 252 continue; 253 } 254 255 if (!array_key_exists('CLUSTER', $this->currSchema) 256 || !array_key_exists($name, $this->currSchema['CLUSTER'])) { 257 $this->applyOrEchoOnce($sql, $stmt = __METHOD__ . "." . $name . ".CREATE"); 258 } 259 } 260 } 261 262 /** 263 * @brief Add sequences 264 * 265 * The function executes the UPDATE statement of the sequence. 266 * 267 * @see applySequences() 268 */ 269 function updateSequences() 270 { 271 if (empty($this->schema['SEQUENCE'])) { 272 return; 273 } 274 foreach ($this->schema['SEQUENCE'] as $name => $import) { 275 if (empty($name)) { 276 continue; 277 } 278 279 if (is_array($import) && array_key_exists('UPDATE', $import)) { 280 $this->applyOrEchoOnce($import['UPDATE'], $stmt = __METHOD__ . "." . $name); 281 } 282 } 283 } 284 285 /** 286 * @brief Add tables/columns (dependent on sequences for default values) 287 * 288 * The function creates new tables in the database. The function also drops 289 * columns which are missing from schema and add new columns as well. 290 */ 291 function applyTables($inherits=false) 292 { 293 if (empty($this->schema['TABLE'])) { 294 return; 295 } 296 foreach ($this->schema['TABLE'] as $table => $columns) { 297 if (empty($table) || $inherits^array_key_exists($table,$this->schema['INHERITS']) ) { 298 continue; 299 } 300 if (!DB_TableExists($table)) { 301 $sql = "CREATE TABLE IF NOT EXISTS \"$table\" ()"; 302 $this->applyOrEchoOnce($sql, $stmt = __METHOD__ . $table); 303 } 304 foreach ($columns as $column => $modification) { 305 if ($this->currSchema['TABLE'][$table][$column]['ADD'] != $modification['ADD']) { 306 $rename = ""; 307 if (DB_ColExists($table, $column)) { 308 /* The column exists, but it looks different! 309 Solution: Delete the column! */ 310 $rename = $column . '_old'; 311 $sql = "ALTER TABLE \"$table\" RENAME COLUMN \"$column\" TO \"$rename\""; 312 $this->applyOrEchoOnce($sql); 313 } 314 315 $sql = $modification['ADD']; 316 if ($this->debug) { 317 print "$sql\n"; 318 } else { 319 // Add the new column which sets the default value 320 $this->dbman->queryOnce($sql); 321 } 322 if (!empty($rename)) { 323 /* copy over the old data */ 324 $this->applyOrEchoOnce($sql = "UPDATE \"$table\" SET \"$column\" = \"$rename\""); 325 $this->applyOrEchoOnce($sql = "ALTER TABLE \"$table\" DROP COLUMN \"$rename\""); 326 } 327 } 328 if ($this->currSchema['TABLE'][$table][$column]['ALTER'] != $modification['ALTER'] && isset($modification['ALTER'])) { 329 $sql = $modification['ALTER']; 330 if ($this->debug) { 331 print "$sql\n"; 332 } else if (!empty ($sql)) { 333 $this->dbman->queryOnce($sql); 334 } 335 } 336 if ($this->currSchema['TABLE'][$table][$column]['DESC'] != $modification['DESC']) { 337 $sql = empty($modification['DESC']) ? "COMMENT ON COLUMN \"$table\".\"$column\" IS ''" : $modification['DESC']; 338 $this->applyOrEchoOnce($sql, $stmt = __METHOD__ . "$table.$column.comment"); 339 } 340 } 341 } 342 } 343 344 /** 345 * Add views (dependent on columns) 346 */ 347 function applyViews() 348 { 349 if (empty($this->schema['VIEW'])) { 350 return; 351 } 352 foreach ($this->schema['VIEW'] as $name => $sql) { 353 if (empty($name) || $this->currSchema['VIEW'][$name] == $sql) { 354 continue; 355 } 356 if (!empty($this->currSchema['VIEW'][$name])) { 357 $sqlDropView = "DROP VIEW IF EXISTS $name"; 358 $this->applyOrEchoOnce($sqlDropView); 359 } 360 $this->applyOrEchoOnce($sql, $stmt = __METHOD__ . $name); 361 } 362 } 363 364 /** 365 * @brief Delete constraints 366 * 367 * Delete now, so they won't interfere with migrations. 368 */ 369 function dropConstraints() 370 { 371 if (empty($this->currSchema['CONSTRAINT'])) { 372 return; 373 } 374 foreach ($this->currSchema['CONSTRAINT'] as $name => $sql) { 375 // skip if constraint name is empty or does not exist 376 if (empty($name) || ($this->schema['CONSTRAINT'][$name] == $sql) 377 || (DB_ConstraintExists($name) == false)) { 378 continue; 379 } 380 381 /* Only process tables that I know about */ 382 $table = preg_replace("/^ALTER TABLE \"(.*)\" ADD CONSTRAINT.*/", '${1}', $sql); 383 $TableFk = preg_replace("/^.*FOREIGN KEY .* REFERENCES \"(.*)\" \(.*/", '${1}', $sql); 384 if ($TableFk == $sql) { 385 $TableFk = $table; 386 } 387 /* If I don't know the primary or foreign table... */ 388 if (empty($this->schema['TABLE'][$table]) && empty($this->schema['TABLE'][$TableFk])) { 389 continue; 390 } 391 $sql = "ALTER TABLE \"$table\" DROP CONSTRAINT \"$name\" CASCADE"; 392 $this->applyOrEchoOnce($sql); 393 } 394 } 395 396 /** 397 * Delete indexes 398 */ 399 function dropIndexes() 400 { 401 if (empty($this->currSchema['INDEX'])) { 402 return; 403 } 404 foreach ($this->currSchema['INDEX'] as $table => $IndexInfo) { 405 if (empty($table) || (empty($this->schema['TABLE'][$table]) && empty($this->schema['INHERITS'][$table]))) { 406 continue; 407 } 408 foreach ($IndexInfo as $name => $sql) { 409 if (empty($name) || $this->schema['INDEX'][$table][$name] == $sql) { 410 continue; 411 } 412 $sql = "DROP INDEX \"$name\""; 413 $this->applyOrEchoOnce($sql); 414 } 415 } 416 } 417 418 /** 419 * Add indexes (dependent on columns) 420 */ 421 function applyIndexes() 422 { 423 if (empty($this->schema['INDEX'])) { 424 return; 425 } 426 foreach ($this->schema['INDEX'] as $table => $indexInfo) { 427 if (empty($table)) { 428 continue; 429 } 430 if (!array_key_exists($table, $this->schema["TABLE"]) && !array_key_exists($table, $this->schema['INHERITS'])) { 431 echo "skipping orphan table: $table\n"; 432 continue; 433 } 434 foreach ($indexInfo as $name => $sql) { 435 if (empty($name) || $this->currSchema['INDEX'][$table][$name] == $sql) { 436 continue; 437 } 438 $this->applyOrEchoOnce($sql); 439 $sql = "REINDEX INDEX \"$name\""; 440 $this->applyOrEchoOnce($sql); 441 } 442 } 443 } 444 445 /** 446 * Add constraints (dependent on columns, views, and indexes) 447 */ 448 function applyConstraints() 449 { 450 $this->currSchema = $this->getCurrSchema(); /* constraints and indexes are linked, recheck */ 451 if (empty($this->schema['CONSTRAINT'])) { 452 return; 453 } 454 /* Constraints must be added in the correct order! */ 455 $orderedConstraints = array('primary' => array(), 'unique' => array(), 'foreign' => array(), 'other' => array()); 456 foreach ($this->schema['CONSTRAINT'] as $Name => $sql) { 457 if (empty($Name) || $this->currSchema['CONSTRAINT'][$Name] == $sql) { 458 continue; 459 } 460 if (preg_match("/PRIMARY KEY/", $sql)) { 461 $orderedConstraints['primary'][] = $sql; 462 } elseif (preg_match("/UNIQUE/", $sql)) { 463 $orderedConstraints['unique'][] = $sql; 464 } elseif (preg_match("/FOREIGN KEY/", $sql)) { 465 $orderedConstraints['foreign'][] = $sql; 466 } else { 467 $orderedConstraints['other'][] = $sql; 468 } 469 } 470 foreach ($orderedConstraints as $type => $constraints) { 471 foreach ($constraints as $sql) { 472 $this->applyOrEchoOnce($sql, $stmt = __METHOD__ . ".constraint.$type"); 473 } 474 } 475 } 476 477 /** 478 * @brief Delete views 479 * 480 * Get current tables and columns used by all views. 481 * Delete if: uses table I know and column I do not know. 482 * Without this delete, we won't be able to drop columns. 483 * 484 * @param string $catalog Name of the catalog 485 */ 486 function dropViews($catalog) 487 { 488 $sql = "SELECT view_name,table_name,column_name 489 FROM information_schema.view_column_usage 490 WHERE table_catalog='$catalog' 491 ORDER BY view_name,table_name,column_name"; 492 $stmt = __METHOD__; 493 $this->dbman->prepare($stmt, $sql); 494 $result = $this->dbman->execute($stmt); 495 while ($row = $this->dbman->fetchArray($result)) { 496 $View = $row['view_name']; 497 $table = $row['table_name']; 498 if (empty($this->schema['TABLE'][$table])) { 499 continue; 500 } 501 $column = $row['column_name']; 502 if (empty($this->schema['TABLE'][$table][$column])) { 503 $sql = "DROP VIEW \"$View\";"; 504 $this->applyOrEchoOnce($sql); 505 } 506 } 507 $result = $this->dbman->freeResult($result); 508 } 509 510 /** 511 * Delete columns from tables 512 * @param array $columns Name of columns to be dropped 513 * @param string $table Name of the table 514 */ 515 function dropColumnsFromTable($columns, $table) 516 { 517 if (empty($table) || empty($this->schema['TABLE'][$table])) { 518 return; 519 } 520 foreach ($columns as $column) { 521 if (empty($column)) { 522 continue; 523 } 524 if (empty($this->schema['TABLE'][$table][$column])) { 525 $sql = "ALTER TABLE \"$table\" DROP COLUMN \"$column\";"; 526 $this->applyOrEchoOnce($sql); 527 } 528 } 529 } 530 531 532 /** 533 * \brief Load the schema from the db into an array. 534 **/ 535 function getCurrSchema() 536 { 537 global $SysConf; 538 $this->currSchema = array(); 539 $this->addInheritedRelations(); 540 $referencedSequencesInTableColumns = $this->addTables(); 541 $this->addViews($viewowner = $SysConf['DBCONF']['user']); 542 $this->addSequences($referencedSequencesInTableColumns); 543 $this->addConstraints(); 544 $this->addIndexes(); 545 unset($this->currSchema['TABLEID']); 546 return $this->currSchema; 547 } 548 549 /** 550 * Add inherited relations to the current schema. 551 */ 552 function addInheritedRelations() 553 { 554 $sql = "SELECT class.relname AS table, daddy.relname AS inherits_from 555 FROM pg_class AS class 556 INNER JOIN pg_catalog.pg_inherits ON pg_inherits.inhrelid = class.oid 557 INNER JOIN pg_class daddy ON pg_inherits.inhparent = daddy.oid"; 558 $this->dbman->prepare($stmt=__METHOD__, $sql); 559 $res = $this->dbman->execute($stmt); 560 $relations = array(); 561 while ($row=$this->dbman->fetchArray($res)) { 562 $relations[$row['table']] = $row['inherits_from']; 563 } 564 $this->dbman->freeResult($res); 565 $this->currSchema['INHERITS'] = $relations; 566 } 567 568 /** 569 * Add tables to the current schema 570 */ 571 function addTables() 572 { 573 $referencedSequencesInTableColumns = array(); 574 575 $sql = "SELECT 576 table_name AS table, ordinal_position AS ordinal, column_name, 577 udt_name AS type, character_maximum_length AS modifier, 578 CASE is_nullable WHEN 'YES' THEN false WHEN 'NO' THEN true END AS notnull, 579 column_default AS default, 580 col_description(table_name::regclass, ordinal_position) AS description 581 FROM information_schema.columns 582 WHERE table_schema = 'public' 583 ORDER BY table_name, ordinal_position;"; 584 $stmt = __METHOD__; 585 $this->dbman->prepare($stmt, $sql); 586 $result = $this->dbman->execute($stmt); 587 while ($R = $this->dbman->fetchArray($result)) { 588 $Table = $R['table']; 589 $Column = $R['column_name']; 590 if (array_key_exists($Table, $this->currSchema['INHERITS'])) { 591 $this->currSchema['TABLEID'][$Table][$R['ordinal']] = $Column; 592 continue; 593 } 594 $Type = $R['type']; 595 if ($Type == 'bpchar') { 596 $Type = "char"; 597 } 598 if ($R['modifier'] > 0) { 599 $Type .= '(' . $R['modifier'] . ')'; 600 } 601 $Desc = str_replace("'", "''", $R['description']); 602 $this->currSchema['TABLEID'][$Table][$R['ordinal']] = $Column; 603 if (!empty($Desc)) { 604 $this->currSchema['TABLE'][$Table][$Column]['DESC'] = "COMMENT ON COLUMN \"$Table\".\"$Column\" IS '$Desc'"; 605 } else { 606 $this->currSchema['TABLE'][$Table][$Column]['DESC'] = ""; 607 } 608 $this->currSchema['TABLE'][$Table][$Column]['ADD'] = "ALTER TABLE \"$Table\" ADD COLUMN \"$Column\" $Type"; 609 $this->currSchema['TABLE'][$Table][$Column]['ALTER'] = "ALTER TABLE \"$Table\""; 610 $Alter = "ALTER COLUMN \"$Column\""; 611 if ($R['notnull'] == 't') { 612 $this->currSchema['TABLE'][$Table][$Column]['ALTER'] .= " $Alter SET NOT NULL"; 613 } else { 614 $this->currSchema['TABLE'][$Table][$Column]['ALTER'] .= " $Alter DROP NOT NULL"; 615 } 616 if ($R['default'] != '') { 617 $R['default'] = preg_replace("/::bpchar/", "::char", $R['default']); 618 $R['default'] = str_replace("public.", "", $R['default']); 619 $this->currSchema['TABLE'][$Table][$Column]['ALTER'] .= ", $Alter SET DEFAULT " . $R['default']; 620 $this->currSchema['TABLE'][$Table][$Column]['ADD'] .= " DEFAULT " . $R['default']; 621 622 $rgx = "/nextval\('([a-z_]*)'.*\)/"; 623 $matches = array(); 624 if (preg_match($rgx, $R['default'], $matches)) { 625 $sequence = $matches[1]; 626 $referencedSequencesInTableColumns[$sequence] = array("table" => $Table, "column" => $Column); 627 } 628 } 629 } 630 $this->dbman->freeResult($result); 631 632 return $referencedSequencesInTableColumns; 633 } 634 635 /** 636 * Add views to the current schema 637 * @param string $viewowner Owner of the view 638 */ 639 function addViews($viewowner) 640 { 641 $sql = "SELECT viewname,definition FROM pg_views WHERE viewowner = $1"; 642 $stmt = __METHOD__; 643 $this->dbman->prepare($stmt, $sql); 644 $result = $this->dbman->execute($stmt, array($viewowner)); 645 while ($row = $this->dbman->fetchArray($result)) { 646 $sql = "CREATE VIEW \"" . $row['viewname'] . "\" AS " . $row['definition']; 647 $this->currSchema['VIEW'][$row['viewname']] = $sql; 648 } 649 $this->dbman->freeResult($result); 650 } 651 652 /** 653 * Add sequences to the current schema 654 * @param array $referencedSequencesInTableColumns Array from addTables() 655 */ 656 function addSequences($referencedSequencesInTableColumns) 657 { 658 $sql = "SELECT relname 659 FROM pg_class 660 WHERE relkind = 'S' 661 AND relnamespace IN ( 662 SELECT oid FROM pg_namespace WHERE nspname NOT LIKE 'pg_%' AND nspname != 'information_schema' 663 )"; 664 665 $stmt = __METHOD__; 666 $this->dbman->prepare($stmt, $sql); 667 $result = $this->dbman->execute($stmt); 668 669 while ($row = $this->dbman->fetchArray($result)) { 670 $sequence = $row['relname']; 671 if (empty($sequence)) { 672 continue; 673 } 674 675 $sqlCreate = "CREATE SEQUENCE \"" . $sequence . "\""; 676 $this->currSchema['SEQUENCE'][$sequence]['CREATE'] = $sqlCreate; 677 678 if (array_key_exists($sequence, $referencedSequencesInTableColumns)) { 679 $table = $referencedSequencesInTableColumns[$sequence]['table']; 680 $column = $referencedSequencesInTableColumns[$sequence]['column']; 681 682 $sqlUpdate = "SELECT setval('$sequence',(SELECT greatest(1,max($column)) val FROM $table))"; 683 $this->currSchema['SEQUENCE'][$sequence]['UPDATE'] = $sqlUpdate; 684 } 685 } 686 687 $this->dbman->freeResult($result); 688 } 689 690 /** 691 * Add constraints to the current schema 692 */ 693 function addConstraints() 694 { 695 $sql = "SELECT c.conname AS constraint_name, 696 CASE c.contype 697 WHEN 'c' THEN 'CHECK' 698 WHEN 'f' THEN 'FOREIGN KEY' 699 WHEN 'p' THEN 'PRIMARY KEY' 700 WHEN 'u' THEN 'UNIQUE' 701 END AS type, 702 CASE WHEN c.condeferrable = 'f' THEN 0 ELSE 1 END AS is_deferrable, 703 CASE WHEN c.condeferred = 'f' THEN 0 ELSE 1 END AS is_deferred, 704 t.relname AS table_name, array_to_string(c.conkey, ' ') AS constraint_key, 705 CASE confupdtype 706 WHEN 'a' THEN 'NO ACTION' 707 WHEN 'r' THEN 'RESTRICT' 708 WHEN 'c' THEN 'CASCADE' 709 WHEN 'n' THEN 'SET NULL' 710 WHEN 'd' THEN 'SET DEFAULT' 711 END AS on_update, 712 CASE confdeltype 713 WHEN 'a' THEN 'NO ACTION' 714 WHEN 'r' THEN 'RESTRICT' 715 WHEN 'c' THEN 'CASCADE' 716 WHEN 'n' THEN 'SET NULL' 717 WHEN 'd' THEN 'SET DEFAULT' END AS on_delete, 718 CASE confmatchtype 719 WHEN 'u' THEN 'UNSPECIFIED' 720 WHEN 'f' THEN 'FULL' 721 WHEN 'p' THEN 'PARTIAL' 722 END AS match_type, 723 t2.relname AS references_table, 724 array_to_string(c.confkey, ' ') AS fk_constraint_key 725 FROM pg_constraint AS c 726 LEFT JOIN pg_class AS t ON c.conrelid = t.oid 727 INNER JOIN information_schema.tables AS tab ON t.relname = tab.table_name 728 LEFT JOIN pg_class AS t2 ON c.confrelid = t2.oid 729 ORDER BY constraint_name,table_name 730 "; 731 $stmt = __METHOD__; 732 $this->dbman->prepare($stmt, $sql); 733 $result = $this->dbman->execute($stmt); 734 $Results = $this->dbman->fetchAll($result); 735 $this->dbman->freeResult($result); 736 /* Constraints use indexes into columns. Covert those to column names. */ 737 for ($i = 0; !empty($Results[$i]['constraint_name']); $i++) { 738 $Key = ""; 739 $Keys = explode(" ", $Results[$i]['constraint_key']); 740 foreach ($Keys as $K) { 741 if (empty($K)) { 742 continue; 743 } 744 if (!empty($Key)) { 745 $Key .= ","; 746 } 747 $Key .= '"' . $this->currSchema['TABLEID'][$Results[$i]['table_name']][$K] . '"'; 748 } 749 $Results[$i]['constraint_key'] = $Key; 750 $Key = ""; 751 $Keys = explode(" ", $Results[$i]['fk_constraint_key']); 752 foreach ($Keys as $K) { 753 if (empty($K)) { 754 continue; 755 } 756 if (!empty($Key)) { 757 $Key .= ","; 758 } 759 $Key .= '"' . $this->currSchema['TABLEID'][$Results[$i]['references_table']][$K] . '"'; 760 } 761 $Results[$i]['fk_constraint_key'] = $Key; 762 } 763 /* Save the constraint */ 764 /* There are different types of constraints that must be stored in order */ 765 /* CONSTRAINT: PRIMARY KEY */ 766 for ($i = 0; !empty($Results[$i]['constraint_name']); $i++) { 767 if ($Results[$i]['type'] != 'PRIMARY KEY') { 768 continue; 769 } 770 $sql = "ALTER TABLE \"" . $Results[$i]['table_name'] . "\""; 771 $sql .= " ADD CONSTRAINT \"" . $Results[$i]['constraint_name'] . '"'; 772 $sql .= " " . $Results[$i]['type']; 773 $sql .= " (" . $Results[$i]['constraint_key'] . ")"; 774 if (!empty($Results[$i]['references_table'])) { 775 $sql .= " REFERENCES \"" . $Results[$i]['references_table'] . "\""; 776 $sql .= " (" . $Results[$i]['fk_constraint_key'] . ")"; 777 } 778 $sql .= ";"; 779 $this->currSchema['CONSTRAINT'][$Results[$i]['constraint_name']] = $sql; 780 $Results[$i]['processed'] = 1; 781 } 782 /* CONSTRAINT: UNIQUE */ 783 for ($i = 0; !empty($Results[$i]['constraint_name']); $i++) { 784 if ($Results[$i]['type'] != 'UNIQUE') { 785 continue; 786 } 787 $sql = "ALTER TABLE \"" . $Results[$i]['table_name'] . "\""; 788 $sql .= " ADD CONSTRAINT \"" . $Results[$i]['constraint_name'] . '"'; 789 $sql .= " " . $Results[$i]['type']; 790 $sql .= " (" . $Results[$i]['constraint_key'] . ")"; 791 if (!empty($Results[$i]['references_table'])) { 792 $sql .= " REFERENCES \"" . $Results[$i]['references_table'] . "\""; 793 $sql .= " (" . $Results[$i]['fk_constraint_key'] . ")"; 794 } 795 $sql .= ";"; 796 $this->currSchema['CONSTRAINT'][$Results[$i]['constraint_name']] = $sql; 797 $Results[$i]['processed'] = 1; 798 } 799 800 /* CONSTRAINT: FOREIGN KEY */ 801 for ($i = 0; !empty($Results[$i]['constraint_name']); $i++) { 802 if ($Results[$i]['type'] != 'FOREIGN KEY') { 803 continue; 804 } 805 $sql = "ALTER TABLE \"" . $Results[$i]['table_name'] . "\""; 806 $sql .= " ADD CONSTRAINT \"" . $Results[$i]['constraint_name'] . '"'; 807 $sql .= " " . $Results[$i]['type']; 808 $sql .= " (" . $Results[$i]['constraint_key'] . ")"; 809 if (!empty($Results[$i]['references_table'])) { 810 $sql .= " REFERENCES \"" . $Results[$i]['references_table'] . "\""; 811 $sql .= " (" . $Results[$i]['fk_constraint_key'] . ")"; 812 } 813 814 if (!empty($Results[$i]['on_update'])) { 815 $sql .= " ON UPDATE " . $Results[$i]['on_update']; 816 } 817 if (!empty($Results[$i]['on_delete'])) { 818 $sql .= " ON DELETE " . $Results[$i]['on_delete']; 819 } 820 821 $sql .= ";"; 822 $this->currSchema['CONSTRAINT'][$Results[$i]['constraint_name']] = $sql; 823 $Results[$i]['processed'] = 1; 824 } 825 826 /* CONSTRAINT: ALL OTHERS */ 827 for ($i = 0; !empty($Results[$i]['constraint_name']); $i++) { 828 if (!empty($Results[$i]['processed']) && $Results[$i]['processed'] == 1) { 829 continue; 830 } 831 832 $sql = "ALTER TABLE \"" . $Results[$i]['table_name'] . "\""; 833 $sql .= " ADD CONSTRAINT \"" . $Results[$i]['constraint_name'] . '"'; 834 $sql .= " " . $Results[$i]['type']; 835 $sql .= " (" . $Results[$i]['constraint_key'] . ")"; 836 if (!empty($Results[$i]['references_table'])) { 837 $sql .= " REFERENCES \"" . $Results[$i]['references_table'] . "\""; 838 $sql .= " (" . $Results[$i]['fk_constraint_key'] . ")"; 839 } 840 $sql .= ";"; 841 $this->currSchema['CONSTRAINT'][$Results[$i]['constraint_name']] = $sql; 842 $Results[$i]['processed'] = 1; 843 } 844 } 845 846 /** 847 * Add indexes to the current schema 848 */ 849 function addIndexes() 850 { 851 $sql = "SELECT tablename AS table, indexname AS index, indexdef AS define 852 FROM pg_indexes 853 INNER JOIN information_schema.tables ON table_name = tablename 854 AND table_type = 'BASE TABLE' 855 AND table_schema = 'public' 856 AND schemaname = 'public' 857 ORDER BY tablename,indexname; 858 "; 859 $stmt = __METHOD__; 860 $this->dbman->prepare($stmt, $sql); 861 $result = $this->dbman->execute($stmt); 862 while ($row = $this->dbman->fetchArray($result)) { 863 /* UNIQUE constraints also include indexes. */ 864 if (empty($this->currSchema['CONSTRAINT'][$row['index']])) { 865 $this->currSchema['INDEX'][$row['table']][$row['index']] = str_replace("public.", "", $row['define']) . ";"; 866 } 867 } 868 $this->dbman->freeResult($result); 869 } 870 871 /** 872 * Add functions to the given schema 873 * @param array $schema Schema in which the functions are to be added 874 * @return array Schema with functions under `FUNCTION` key 875 */ 876 function addFunctions($schema) 877 { 878 // prosrc 879 // proretset == setof 880 $sql = "SELECT proname AS name, 881 pronargs AS input_num, 882 proargnames AS input_names, 883 proargtypes AS input_type, 884 proargmodes AS input_modes, 885 proretset AS setof, 886 prorettype AS output_type 887 FROM pg_proc AS proc 888 INNER JOIN pg_language AS lang ON proc.prolang = lang.oid 889 WHERE lang.lanname = 'plpgsql' 890 ORDER BY proname;"; 891 $stmt = __METHOD__; 892 $this->dbman->prepare($stmt, $sql); 893 $result = $this->dbman->execute($stmt); 894 while ($row = $this->dbman->fetchArray($result)) { 895 $sql = "CREATE or REPLACE function " . $row['proname'] . "()"; 896 $sql .= ' RETURNS ' . "TBD" . ' AS $$'; 897 $sql .= " " . $row['prosrc']; 898 $schema['FUNCTION'][$row['proname']] = $sql; 899 } 900 $this->dbman->freeResult($result); 901 return $schema; 902 } 903 904 /** 905 * Write array entries to $fout as string representation 906 * @param resource $fout 907 * @param string $key 908 * @param array $value 909 * @param string $varname 910 */ 911 function writeArrayEntries($fout, $key, $value, $varname) 912 { 913 $varname .= '["' . str_replace('"', '\"', $key) . '"]'; 914 if (!is_array($value)) { 915 $value = str_replace('"', '\"', $value); 916 fwrite($fout, "$varname = \"$value\";\n"); 917 return; 918 } 919 foreach ($value as $k => $v) { 920 $this->writeArrayEntries($fout, $k, $v, $varname); 921 } 922 fwrite($fout, "\n"); 923 } 924 925 /** 926 * \brief Export the schema of the connected ($PG_CONN) database to a 927 * file in the format readable by GetSchema(). 928 * 929 * @param string $filename Path to the file to store the schema in. 930 * 931 * @return false=success, on error return string with error message. 932 **/ 933 function exportSchema($filename = NULL) 934 { 935 global $PG_CONN; 936 937 /* set driver */ 938 $dbDriver = $this->dbman->getDriver(); 939 if (empty($dbDriver)) { 940 $this->dbman->setDriver(new Postgres($PG_CONN)); 941 } 942 943 if (empty($filename)) { 944 $filename = stdout; 945 } 946 $Schema = $this->getCurrSchema(); 947 $fout = fopen($filename, "w"); 948 if (!$fout) { 949 return ("Failed to write to $filename\n"); 950 } 951 global $Name; 952 fwrite($fout, "<?php\n"); 953 fwrite($fout, "/* This file is generated by " . $Name . " */\n"); 954 fwrite($fout, "/* Do not manually edit this file */\n\n"); 955 fwrite($fout, ' $Schema=array();' . "\n\n"); 956 foreach ($Schema as $K1 => $V1) { 957 $this->writeArrayEntries($fout, $K1, $V1, ' $Schema'); 958 } 959 fclose($fout); 960 return false; 961 } 962 963 964 /** 965 * \brief Create any required DB functions. 966 */ 967 function makeFunctions() 968 { 969 print " Applying database functions\n"; 970 flush(); 971 /* ******************************************* 972 * uploadtree2path is a DB function that returns the non-artifact parents of an uploadtree_pk. 973 * drop and recreate to change the return type. 974 */ 975 $sql = 'drop function if exists uploadtree2path(integer);'; 976 $this->applyOrEchoOnce($sql, $stmt = __METHOD__ . '.uploadtree2path.drop'); 977 978 $sql = ' 979 CREATE function uploadtree2path(uploadtree_pk_in int) returns setof uploadtree as $$ 980 DECLARE 981 UTrec uploadtree; 982 UTpk integer; 983 sql varchar; 984 BEGIN 985 UTpk := uploadtree_pk_in; 986 WHILE UTpk > 0 LOOP 987 sql := ' . "'" . 'select * from uploadtree where uploadtree_pk=' . "'" . ' || UTpk; 988 execute sql into UTrec; 989 IF ((UTrec.ufile_mode & (1<<28)) = 0) THEN RETURN NEXT UTrec; END IF; 990 UTpk := UTrec.parent; 991 END LOOP; 992 RETURN; 993 END; 994 $$ 995 LANGUAGE plpgsql; 996 '; 997 $this->applyOrEchoOnce($sql, $stmt = __METHOD__ . '.uploadtree2path.create'); 998 999 /* 1000 * getItemParent is a DB function that returns the non-artifact parent of an uploadtree_pk. 1001 * drop and recreate to change the return type. 1002 */ 1003 $sql = 'drop function if exists getItemParent(integer);'; 1004 $this->applyOrEchoOnce($sql, $stmt = __METHOD__ . '.getItemParent.drop'); 1005 1006 $sql = ' 1007 CREATE OR REPLACE FUNCTION getItemParent(itemId Integer) RETURNS Integer AS $$ 1008 WITH RECURSIVE file_tree(uploadtree_pk, parent, jump, path, cycle) AS ( 1009 SELECT ut.uploadtree_pk, ut.parent, 1010 true, 1011 ARRAY[ut.uploadtree_pk], 1012 false 1013 FROM uploadtree ut 1014 WHERE ut.uploadtree_pk = $1 1015 UNION ALL 1016 SELECT ut.uploadtree_pk, ut.parent, 1017 ut.ufile_mode & (1<<28) != 0, 1018 path || ut.uploadtree_pk, 1019 ut.uploadtree_pk = ANY(path) 1020 FROM uploadtree ut, file_tree ft 1021 WHERE ut.uploadtree_pk = ft.parent AND jump AND NOT cycle 1022 ) 1023 SELECT uploadtree_pk from file_tree ft WHERE NOT jump 1024 $$ 1025 LANGUAGE SQL 1026 STABLE 1027 RETURNS NULL ON NULL INPUT 1028 '; 1029 $this->applyOrEchoOnce($sql, $stmt = __METHOD__ . '.getItemParent.create'); 1030 return; 1031 } 1032 1033 /** 1034 * Apply inherits relations from schema on DB 1035 */ 1036 function applyInheritedRelations() 1037 { 1038 if (empty($this->schema['INHERITS'])) { 1039 return; 1040 } 1041 foreach ($this->schema['INHERITS'] as $table => $fromTable) { 1042 if (empty($table)) { 1043 continue; 1044 } 1045 if (!$this->dbman->existsTable($table) && $this->dbman->existsTable($fromTable)) { 1046 $sql = "CREATE TABLE \"$table\" () INHERITS (\"$fromTable\")"; 1047 $this->applyOrEchoOnce($sql, $stmt = __METHOD__ . $table); 1048 } 1049 } 1050 } 1051 1052 // MakeFunctions() 1053} 1054 1055if (empty($dbManager) || !($dbManager instanceof DbManager)) { 1056 $logLevel = Logger::INFO; 1057 $logger = new Logger(__FILE__); 1058 $logger->pushHandler(new ErrorLogHandler(ErrorLogHandler::OPERATING_SYSTEM, $logLevel)); 1059 $dbManager = new ModernDbManager($logger); 1060 $pgDriver = new Postgres($PG_CONN); 1061 $dbManager->setDriver($pgDriver); 1062} 1063/* simulate the old functions*/ 1064$libschema = new fo_libschema($dbManager); 1065/** 1066 * @brief Make schema match $Filename. This is a single transaction. 1067 * @param string $Filename Schema file written by schema-export.php 1068 * @param bool $Debug Turn on debugging (echo sql as it is being executed) 1069 * @param string $Catalog Optional database name 1070 * @return false=success, on error return string with error message. 1071 **/ 1072function ApplySchema($Filename = NULL, $Debug = false, $Catalog = 'fossology') 1073{ 1074 global $libschema; 1075 return $libschema->applySchema($Filename, $Debug, $Catalog); 1076} 1077 1078/** 1079 * \brief Load the schema from the db into an array. 1080 **/ 1081function GetSchema() 1082{ 1083 global $libschema; 1084 return $libschema->getCurrSchema(); 1085} 1086 1087/** 1088 * \brief Export the schema of the connected ($PG_CONN) database to a 1089 * file in the format readable by GetSchema(). 1090 * @param string $filename path to the file to store the schema in. 1091 * @return false=success, on error return string with error message. 1092 **/ 1093function ExportSchema($filename = NULL) 1094{ 1095 global $libschema; 1096 return $libschema->exportSchema($filename); 1097} 1098 1099/** 1100 * \brief Create any required DB functions. 1101 */ 1102function MakeFunctions($Debug) 1103{ 1104 global $libschema; 1105 return $libschema->makeFunctions($Debug); 1106} 1107