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