Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
53.33% covered (warning)
53.33%
296 / 555
25.00% covered (danger)
25.00%
9 / 36
CRAP
0.00% covered (danger)
0.00%
0 / 1
SeedDMS_Core_DatabaseAccess
53.33% covered (warning)
53.33%
296 / 555
25.00% covered (danger)
25.00%
9 / 36
5463.87
0.00% covered (danger)
0.00%
0 / 1
 TableList
62.50% covered (warning)
62.50%
10 / 16
0.00% covered (danger)
0.00%
0 / 1
7.90
 hasTable
53.33% covered (warning)
53.33%
8 / 15
0.00% covered (danger)
0.00%
0 / 1
9.66
 hasTableField
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 ViewList
62.50% covered (warning)
62.50%
10 / 16
0.00% covered (danger)
0.00%
0 / 1
7.90
 __construct
82.61% covered (warning)
82.61%
19 / 23
0.00% covered (danger)
0.00%
0 / 1
4.08
 getDriver
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 useViews
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __destruct
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
5.67
 setLogFp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 connect
59.38% covered (warning)
59.38%
19 / 32
0.00% covered (danger)
0.00%
0 / 1
27.14
 ensureConnected
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 qstr
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 rbt
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 concat
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
4.84
 getResultArray
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
7.60
 fetchResult
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
 getResult
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
9.36
 startTransaction
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 rollbackTransaction
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 commitTransaction
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 inTransaction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getInsertID
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getErrorMsg
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getErrorNo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __createTemporaryTable
50.35% covered (warning)
50.35%
72 / 143
0.00% covered (danger)
0.00%
0 / 1
246.75
 __dropTemporaryTable
61.11% covered (warning)
61.11%
11 / 18
0.00% covered (danger)
0.00%
0 / 1
13.76
 __createView
50.83% covered (warning)
50.83%
61 / 120
0.00% covered (danger)
0.00%
0 / 1
240.79
 createTemporaryTable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 dropTemporaryTable
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getDateExtract
40.00% covered (danger)
40.00%
4 / 10
0.00% covered (danger)
0.00%
0 / 1
13.78
 getCurrentDatetime
35.71% covered (danger)
35.71%
5 / 14
0.00% covered (danger)
0.00%
0 / 1
20.02
 getCurrentTimestamp
50.00% covered (danger)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
6.00
 castToText
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 createDump
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 runQueries
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 runQueriesFromFile
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2declare(strict_types=1);
3
4/**
5 * Implementation of database access using PDO
6 *
7 * @category   DMS
8 * @package    SeedDMS_Core
9 * @license    GPL 2
10 * @version    @version@
11 * @author     Uwe Steinmann <uwe@steinmann.cx>
12 * @copyright  Copyright (C) 2012 Uwe Steinmann
13 * @version    Release: @package_version@
14 */
15/** @noinspection PhpUndefinedClassInspection */
16
17/**
18 * Class to represent the database access for the document management
19 * This class uses PDO for the actual database access.
20 *
21 * @category   DMS
22 * @package    SeedDMS_Core
23 * @author     Uwe Steinmann <uwe@steinmann.cx>
24 * @copyright  Copyright (C) 2012 Uwe Steinmann
25 * @version    Release: @package_version@
26 */
27class SeedDMS_Core_DatabaseAccess {
28    /**
29     * @var boolean set to true for debug mode
30     */
31    public $_debug;
32
33    /**
34     * @var string name of database driver (mysql or sqlite)
35     */
36    protected $_driver;
37
38    /**
39     * @var string name of hostname
40     */
41    protected $_hostname;
42
43    /**
44     * @var int port number of database
45     */
46    protected $_port;
47
48    /**
49     * @var string name of database
50     */
51    protected $_database;
52
53    /**
54     * @var string name of database user
55     */
56    protected $_user;
57
58    /**
59     * @var string password of database user
60     */
61    protected $_passw;
62
63    /**
64     * @var object internal database connection
65     */
66    private $_conn;
67
68    /**
69     * @var boolean set to true if connection to database is established
70     */
71    private $_connected;
72
73    /**
74     * @var boolean set to true if temp. table for tree view has been created
75     */
76    private $_ttreviewid;
77
78    /**
79     * @var boolean set to true if temp. table for approvals has been created
80     */
81    private $_ttapproveid;
82
83    /**
84     * @var boolean set to true if temp. table for doc status has been created
85     */
86    private $_ttstatid;
87
88    /**
89     * @var boolean set to true if temp. table for doc content has been created
90     */
91    private $_ttcontentid;
92
93    /**
94     * @var boolean set to true if in a database transaction
95     */
96    private $_intransaction;
97
98    /**
99     * @var string set a valid file name for logging all sql queries
100     */
101    private $_logfile;
102
103    /**
104     * @var resource file pointer of log file
105     */
106    private $_logfp;
107
108    /**
109     * @var boolean set to true if views instead of temp. tables shall be used
110     */
111    private $_useviews;
112
113    /**
114     * Return list of all database tables
115     *
116     * This function is used to retrieve a list of database tables for backup
117     *
118     * @return string[]|bool list of table names
119     */
120    public function TableList() { /* {{{ */
121        switch ($this->_driver) {
122            case 'mysql':
123                $sql = "SELECT `TABLE_NAME` AS `name` FROM `information_schema`.`tables` WHERE `TABLE_SCHEMA`='".$this->_database."' AND `TABLE_TYPE`='BASE TABLE'";
124                break;
125            case 'sqlite':
126                $sql = "SELECT tbl_name AS name FROM sqlite_master WHERE type='table'";
127                break;
128            case 'pgsql':
129                $sql = "select tablename as name from pg_catalog.pg_tables where schemaname='public'";
130                break;
131            default:
132                return false;
133        }
134        $arr = $this->getResultArray($sql);
135        $res = array();
136        foreach ($arr as $tmp)
137            $res[] = $tmp['name'];
138        return $res;
139    }    /* }}} */
140
141    /**
142     * Check if database has a table
143     *
144     * This function will check if the database has a table with the given table name
145     *
146     * @return bool true if table exists, otherwise false
147     */
148    public function hasTable($name) { /* {{{ */
149        switch ($this->_driver) {
150            case 'mysql':
151                $sql = "SELECT `TABLE_NAME` AS `name` FROM `information_schema`.`tables` WHERE `TABLE_SCHEMA`='".$this->_database."' AND `TABLE_TYPE`='BASE TABLE' AND `TABLE_NAME`=".$this->qstr($name);
152                break;
153            case 'sqlite':
154                $sql = "SELECT tbl_name AS name FROM sqlite_master WHERE type='table' AND `tbl_name`=".$this->qstr($name);
155                break;
156            case 'pgsql':
157                $sql = "SELECT tablename AS name FROM pg_catalog.pg_tables WHERE schemaname='public' AND tablename=".$this->qstr($name);
158                break;
159            default:
160                return false;
161        }
162        $arr = $this->getResultArray($sql);
163        if ($arr)
164            return true;
165        return false;
166    }    /* }}} */
167
168    /**
169     * Check if table has a field
170     *
171     * This function will check if the table has a field with the given name
172     *
173     * @return bool true if table exists, otherwise false
174     */
175    public function hasTableField($name, $fieldname) { /* {{{ */
176        switch ($this->_driver) {
177            case 'mysql':
178                $sql = "SHOW COLUMNS FROM `".$name."` LIKE ".$this->qstr($fieldname);
179                break;
180            case 'sqlite':
181                $sql = "SELECT * FROM pragma_table_info(".$this->qstr($name).") WHERE name=".$this->qstr($fieldname);
182                break;
183            case 'pgsql':
184                $sql = "SELECT column_name FROM information_schema.columns WHERE table_name=".$this->qstr($name)." and column_name=".$this->qstr($fieldname);
185                break;
186            default:
187                return false;
188        }
189        $arr = $this->getResultArray($sql);
190        return $arr;
191    }    /* }}} */
192
193    /**
194     * Return list of all database views
195     *
196     * This function is used to retrieve a list of database views
197     *
198     * @return array list of view names
199     */
200    public function ViewList() { /* {{{ */
201        switch ($this->_driver) {
202            case 'mysql':
203                $sql = "select TABLE_NAME as name from information_schema.views where TABLE_SCHEMA='".$this->_database."'";
204                break;
205            case 'sqlite':
206                $sql = "select tbl_name as name from sqlite_master where type='view'";
207                break;
208            case 'pgsql':
209                $sql = "select viewname as name from pg_catalog.pg_views where schemaname='public'";
210                break;
211            default:
212                return false;
213        }
214        $arr = $this->getResultArray($sql);
215        $res = array();
216        foreach ($arr as $tmp)
217            $res[] = $tmp['name'];
218        return $res;
219    }    /* }}} */
220
221    /**
222     * Constructor of SeedDMS_Core_DatabaseAccess
223     *
224     * Sets all database parameters but does not connect.
225     *
226     * @param string $driver the database type e.g. mysql, sqlite
227     * @param string $hostname host of database server
228     * @param string $user name of user having access to database
229     * @param string $passw password of user
230     * @param bool|string $database name of database
231     */
232    public function __construct($driver, $hostname, $user, $passw, $database = false) { /* {{{ */
233        $this->_driver = $driver;
234        $tmp = explode(":", $hostname);
235        $this->_hostname = $tmp[0];
236        $this->_port = null;
237        if (!empty($tmp[1]))
238            $this->_port = $tmp[1];
239        $this->_database = $database;
240        $this->_user = $user;
241        $this->_passw = $passw;
242        $this->_connected = false;
243        $this->_intransaction = 0;
244        $this->_logfile = '';
245        if ($this->_logfile) {
246            $this->_logfp = fopen($this->_logfile, 'a+');
247            if ($this->_logfp)
248                fwrite($this->_logfp, microtime(true)."    BEGIN ".$_SERVER['REQUEST_URI']." ------------------------------------------\n");
249        } else {
250            $this->_logfp = null;
251        }
252        // $tt*****id is a hack to ensure that we do not try to create the
253        // temporary table twice during a single connection. Can be fixed by
254        // using Views (MySQL 5.0 onward) instead of temporary tables.
255        // CREATE ... IF NOT EXISTS cannot be used because it has the
256        // unpleasant side-effect of performing the insert again even if the
257        // table already exists.
258        //
259        // See createTemporaryTable() method for implementation.
260        $this->_ttreviewid = false;
261        $this->_ttapproveid = false;
262        $this->_ttstatid = false;
263        $this->_ttcontentid = false;
264        $this->_useviews = false; // turn off views, because they are much slower then temp. tables. They also break the transaction management, because dropping a view will commit the current transaction.
265        $this->_debug = false;
266    } /* }}} */
267
268    /**
269     * Return driver
270     *
271     * @return string name of driver as set in constructor
272     */
273    public function getDriver() { /* {{{ */
274        return $this->_driver;
275    } /* }}} */
276
277    /**
278     * Turn on views instead of temp. tables
279     *
280     * @param bool $onoff turn use of views instead of temp. table on/off
281     */
282    public function useViews($onoff) { /* {{{ */
283        $this->_useviews = $onoff;
284    } /* }}} */
285
286    /**
287     * Destructor of SeedDMS_Core_DatabaseAccess
288     */
289    public function __destruct() { /* {{{ */
290        if ($this->_logfile && $this->_logfp) {
291            fwrite($this->_logfp, microtime(true)."    END --------------------------------------------\n");
292            fclose($this->_logfp);
293        }
294    } /* }}} */
295
296    /**
297     * Set the file pointer to a log file
298     *
299     * Once it is set, all queries will be logged into this file
300     */
301    public function setLogFp($fp) { /* {{{ */
302        $this->_logfp = $fp;
303    } /* }}} */
304
305    /**
306     * Connect to database
307     *
308     * @return boolean true if connection could be established, otherwise false
309     */
310    public function connect() { /* {{{ */
311        switch ($this->_driver) {
312            case 'mysql':
313            case 'mysqli':
314            case 'mysqlnd':
315            case 'pgsql':
316                $dsn = $this->_driver.":dbname=".$this->_database.";host=".$this->_hostname;
317                if ($this->_port)
318                    $dsn .= ";port=".$this->_port;
319                break;
320            case 'sqlite':
321                $dsn = $this->_driver.":".$this->_database;
322                break;
323        }
324        try {
325            /** @noinspection PhpUndefinedVariableInspection */
326            $this->_conn = new PDO($dsn, $this->_user, $this->_passw);
327            if (!$this->_conn)
328                return false;
329            /* Prevent PDO from throwing an exception because the code currently
330             * cannot handle it. PDO::ERRMODE_EXCEPTION became the default as of php 8.0.0
331             * PDO::ERRMODE_SILENT was the default before php 8.0.0
332             */
333            $this->_conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
334
335            switch ($this->_driver) {
336                case 'mysql':
337                    $this->_conn->exec('SET NAMES utf8mb4');
338//                    $this->_conn->setAttribute(PDO::ATTR_AUTOCOMMIT, FALSE);
339                    /* Turn this on if you want strict checking of default values, etc. */
340                    /* $this->_conn->exec("SET SESSION sql_mode = 'STRICT_TRANS_TABLES'"); */
341                    /* The following is the default on Ubuntu 16.04 */
342                    /* $this->_conn->exec("SET SESSION sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'"); */
343                    break;
344                case 'sqlite':
345                    $this->_conn->exec('PRAGMA foreign_keys = ON');
346                    break;
347            }
348        } catch (Exception $e) {
349            return false;
350        }
351        if ($this->_useviews) {
352            $tmp = $this->ViewList();
353            foreach (array('ttreviewid', 'ttapproveid', 'ttstatid', 'ttcontentid') as $viewname) {
354                if (in_array($viewname, $tmp)) {
355                    $this->{"_".$viewname} = true;
356                }
357            }
358        }
359
360        $this->_connected = true;
361        return true;
362    } /* }}} */
363
364    /**
365     * Make sure a database connection exisits
366     *
367     * This function checks for a database connection. If it does not exists
368     * it will reconnect.
369     *
370     * @return boolean true if connection is established, otherwise false
371     */
372    public function ensureConnected() { /* {{{ */
373        if (!$this->_connected) {
374            return $this->connect();
375        } else {
376            return true;
377        }
378    } /* }}} */
379
380    /**
381     * Sanitize String used in database operations
382     *
383     * @param string $text
384     * @return string sanitized string
385     */
386    public function qstr(?string $text): string { /* {{{ */
387        return is_null($text) ? 'NULL' : $this->_conn->quote($text);
388    } /* }}} */
389
390    /**
391     * Replace back ticks by '"'
392     *
393     * @param string $text
394     * @return string sanitized string
395     */
396    public function rbt($text) { /* {{{ */
397        return str_replace('`', '"', $text);
398    } /* }}} */
399
400    /**
401     * Return sql to concat strings or fields
402     *
403     * @param array $arr list of field names or strings
404     * @return string concated string
405     */
406    public function concat($arr) { /* {{{ */
407        switch ($this->_driver) {
408            case 'mysql':
409                return 'concat('.implode(',', $arr).')';
410                    break;
411            case 'pgsql':
412                return implode(' || ', $arr);
413                    break;
414            case 'sqlite':
415                return implode(' || ', $arr);
416                    break;
417        }
418        return '';
419    } /* }}} */
420
421    /**
422     * Execute SQL query and return result
423     *
424     * Call this function only with sql query which return data records.
425     *
426     * @param string $queryStr sql query
427     * @param bool $retick
428     * @return array|bool data if query could be executed otherwise false
429     */
430    public function getResultArray($queryStr, $retick = true) { /* {{{ */
431        $resArr = array();
432
433        if ($retick && $this->_driver == 'pgsql') {
434            $queryStr = $this->rbt($queryStr);
435        }
436
437        if ($this->_logfp) {
438            fwrite($this->_logfp, microtime(true)."    ".($this->_conn->inTransaction() ? '*' : ' ')." ".$queryStr."\n");
439        }
440        $res = $this->_conn->query($queryStr);
441        if ($res === false) {
442            if ($this->_debug) {
443                echo "error: ".$queryStr."<br />";
444                print_r($this->_conn->errorInfo());
445            }
446            return false;
447        }
448        $resArr = $res->fetchAll(PDO::FETCH_ASSOC);
449//        $res->Close();
450        return $resArr;
451    } /* }}} */
452
453    /**
454     * Execute SQL query and return records one by one
455     *
456     * Call this function only with sql query which return data records.
457     *
458     * @param string $queryStr sql query
459     * @param bool $retick
460     * @return array|bool data if query could be executed otherwise false
461     */
462    public function fetchResult($queryStr, $retick = true) { /* {{{ */
463        $resArr = array();
464
465        if ($retick && $this->_driver == 'pgsql') {
466            $queryStr = $this->rbt($queryStr);
467        }
468
469        if ($this->_logfp) {
470            fwrite($this->_logfp, microtime(true)."    ".($this->_conn->inTransaction() ? '*' : ' ')." ".$queryStr."\n");
471        }
472        $res = $this->_conn->query($queryStr);
473        if ($res === false) {
474            if ($this->_debug) {
475                echo "error: ".$queryStr."<br />";
476                print_r($this->_conn->errorInfo());
477            }
478            return false;
479        }
480        while ($resArr = $res->fetch(PDO::FETCH_ASSOC)) {
481            yield $resArr;
482        }
483        return null;
484    } /* }}} */
485
486    /**
487     * Execute SQL query
488     *
489     * Call this function only with sql query which do not return data records.
490     *
491     * @param string $queryStr sql query
492     * @param boolean $retick replace all '`' by '"'
493     * @return boolean true if query could be executed otherwise false
494     */
495    public function getResult($queryStr, $retick = true) { /* {{{ */
496        if ($retick && $this->_driver == 'pgsql') {
497            $queryStr = $this->rbt($queryStr);
498        }
499
500        if ($this->_logfp) {
501            fwrite($this->_logfp, microtime(true)."    ".($this->_conn->inTransaction() ? '*' : ' ')." ".$queryStr."\n");
502        }
503        $res = $this->_conn->exec($queryStr);
504        if ($res === false) {
505            if ($this->_debug) {
506                echo "error: ".$queryStr."<br />";
507                print_r($this->_conn->errorInfo());
508            }
509            return false;
510        } else {
511            return true;
512        }
513
514        return $res;
515    } /* }}} */
516
517    public function startTransaction() { /* {{{ */
518        if (!$this->_intransaction) {
519            $this->_conn->beginTransaction();
520        }
521        $this->_intransaction++;
522        if ($this->_logfp) {
523            fwrite($this->_logfp, microtime(true)."    ".($this->_conn->inTransaction() ? '*' : ' ')." START ".$this->_intransaction."\n");
524        }
525    } /* }}} */
526
527    public function rollbackTransaction() { /* {{{ */
528        if ($this->_logfp) {
529            fwrite($this->_logfp, microtime(true)."    ".($this->_conn->inTransaction() ? '*' : ' ')." ROLLBACK ".$this->_intransaction."\n");
530        }
531        if ($this->_intransaction == 1) {
532            $this->_conn->rollBack();
533        }
534        $this->_intransaction--;
535    } /* }}} */
536
537    public function commitTransaction() { /* {{{ */
538        if ($this->_logfp) {
539            fwrite($this->_logfp, microtime(true)."    ".($this->_conn->inTransaction() ? '*' : ' ')." COMMIT ".$this->_intransaction."\n");
540        }
541        if ($this->_intransaction == 1) {
542            $this->_conn->commit();
543        }
544        $this->_intransaction--;
545    } /* }}} */
546
547    public function inTransaction() { /* {{{ */
548        return $this->_conn->inTransaction();
549    } /* }}} */
550
551    /**
552     * Return the id of the last instert record
553     *
554     * @param string $tablename
555     * @param string $fieldname
556     * @return int id used in last autoincrement
557     */
558    public function getInsertID($tablename = '', $fieldname = 'id') { /* {{{ */
559        if ($this->_driver == 'pgsql') {
560            return $this->_conn->lastInsertId('"'.$tablename.'_'.$fieldname.'_seq"');
561        } else {
562            return $this->_conn->lastInsertId();
563        }
564    } /* }}} */
565
566    public function getErrorMsg() { /* {{{ */
567        $info = $this->_conn->errorInfo();
568        return($info[2]);
569    } /* }}} */
570
571    public function getErrorNo() { /* {{{ */
572        return $this->_conn->errorCode();
573    } /* }}} */
574
575    /**
576     * Create various temporary tables to speed up and simplify sql queries
577     *
578     * @param string $tableName
579     * @param bool $override
580     * @return bool
581     */
582    private function __createTemporaryTable($tableName, $override = false) { /* {{{ */
583        if (!strcasecmp($tableName, "ttreviewid")) {
584            switch ($this->_driver) {
585                case 'sqlite':
586                    $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttreviewid` AS ".
587                        "SELECT `tblDocumentReviewLog`.`reviewID` AS `reviewID`, ".
588                        "MAX(`tblDocumentReviewLog`.`reviewLogID`) AS `maxLogID` ".
589                        "FROM `tblDocumentReviewLog` ".
590                        "GROUP BY `tblDocumentReviewLog`.`reviewID` "; //.
591//                        "ORDER BY `maxLogID`";
592                    $queryStr .= "; CREATE INDEX `ttreviewid_idx` ON `ttreviewid` (`reviewID`);";
593                    $dropStr = "DROP TABLE IF EXISTS `ttreviewid`";
594                break;
595                case 'pgsql':
596                    $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttreviewid` (`reviewID` INTEGER, `maxLogID` INTEGER, PRIMARY KEY (`reviewID`));".
597                        "INSERT INTO `ttreviewid` SELECT `tblDocumentReviewLog`.`reviewID`, ".
598                        "MAX(`tblDocumentReviewLog`.`reviewLogID`) AS `maxLogID` ".
599                        "FROM `tblDocumentReviewLog` ".
600                        "GROUP BY `tblDocumentReviewLog`.`reviewID` ";//.
601//                        "ORDER BY `maxLogID`";
602                    $dropStr = "DROP TABLE IF EXISTS `ttreviewid`";
603                break;
604                default:
605                    $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttreviewid` (PRIMARY KEY (`reviewID`), INDEX (`maxLogID`)) ".
606                        "SELECT `tblDocumentReviewLog`.`reviewID`, ".
607                        "MAX(`tblDocumentReviewLog`.`reviewLogID`) AS `maxLogID` ".
608                        "FROM `tblDocumentReviewLog` ".
609                        "GROUP BY `tblDocumentReviewLog`.`reviewID` "; //.
610//                        "ORDER BY `maxLogID`";
611                    $dropStr = "DROP TEMPORARY TABLE IF EXISTS `ttreviewid`";
612            }
613            if (!$this->_ttreviewid) {
614                if (!$this->getResult($queryStr))
615                    return false;
616                $this->_ttreviewid = true;
617            } else {
618                if (is_bool($override) && $override) {
619                    if (!$this->getResult($dropStr))
620                        return false;
621                    if (!$this->getResult($queryStr))
622                        return false;
623                }
624            }
625            return $this->_ttreviewid;
626        } elseif (!strcasecmp($tableName, "ttapproveid")) {
627            switch ($this->_driver) {
628                case 'sqlite':
629                    $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttapproveid` AS ".
630                        "SELECT `tblDocumentApproveLog`.`approveID` AS `approveID`, ".
631                        "MAX(`tblDocumentApproveLog`.`approveLogID`) AS `maxLogID` ".
632                        "FROM `tblDocumentApproveLog` ".
633                        "GROUP BY `tblDocumentApproveLog`.`approveID` "; //.
634//                        "ORDER BY `maxLogID`";
635                    $queryStr .= "; CREATE INDEX `ttapproveid_idx` ON `ttapproveid` (`approveID`);";
636                    $dropStr = "DROP TABLE IF EXISTS `ttapproveid`";
637                    break;
638                case 'pgsql':
639                    $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttapproveid` (`approveID` INTEGER, `maxLogID` INTEGER, PRIMARY KEY (`approveID`));".
640                        "INSERT INTO `ttapproveid` SELECT `tblDocumentApproveLog`.`approveID`, ".
641                        "MAX(`tblDocumentApproveLog`.`approveLogID`) AS `maxLogID` ".
642                        "FROM `tblDocumentApproveLog` ".
643                        "GROUP BY `tblDocumentApproveLog`.`approveID` "; //.
644//                        "ORDER BY `maxLogID`";
645                    $dropStr = "DROP TABLE IF EXISTS `ttapproveid`";
646                    break;
647                default:
648                    $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttapproveid` (PRIMARY KEY (`approveID`), INDEX (`maxLogID`)) ".
649                        "SELECT `tblDocumentApproveLog`.`approveID`, ".
650                        "MAX(`tblDocumentApproveLog`.`approveLogID`) AS `maxLogID` ".
651                        "FROM `tblDocumentApproveLog` ".
652                        "GROUP BY `tblDocumentApproveLog`.`approveID` "; //.
653//                        "ORDER BY `maxLogID`";
654                    $dropStr = "DROP TEMPORARY TABLE IF EXISTS `ttapproveid`";
655            }
656            if (!$this->_ttapproveid) {
657                if (!$this->getResult($queryStr))
658                    return false;
659                $this->_ttapproveid = true;
660            } else {
661                if (is_bool($override) && $override) {
662                    if (!$this->getResult($dropStr))
663                        return false;
664                    if (!$this->getResult($queryStr))
665                        return false;
666                }
667            }
668            return $this->_ttapproveid;
669        } elseif (!strcasecmp($tableName, "ttstatid")) {
670            switch ($this->_driver) {
671                case 'sqlite':
672                    $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttstatid` AS ".
673                        "SELECT `tblDocumentStatusLog`.`statusID` AS `statusID`, ".
674                        "MAX(`tblDocumentStatusLog`.`statusLogID`) AS `maxLogID` ".
675                        "FROM `tblDocumentStatusLog` ".
676                        "GROUP BY `tblDocumentStatusLog`.`statusID` "; //.
677//                        "ORDER BY `maxLogID`";
678                    $queryStr .= "; CREATE INDEX `ttstatid_idx` ON `ttstatid` (`statusID`);";
679                    $dropStr = "DROP TABLE IF EXISTS `ttstatid`";
680                    break;
681                case 'pgsql':
682                    $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttstatid` (`statusID` INTEGER, `maxLogID` INTEGER, PRIMARY KEY (`statusID`));".
683                        "INSERT INTO `ttstatid` SELECT `tblDocumentStatusLog`.`statusID`, ".
684                        "MAX(`tblDocumentStatusLog`.`statusLogID`) AS `maxLogID` ".
685                        "FROM `tblDocumentStatusLog` ".
686                        "GROUP BY `tblDocumentStatusLog`.`statusID` "; //.
687//                        "ORDER BY `maxLogID`";
688                    $dropStr = "DROP TABLE IF EXISTS `ttstatid`";
689                    break;
690                default:
691                    $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttstatid` (PRIMARY KEY (`statusID`), INDEX (`maxLogID`)) ".
692                        "SELECT `tblDocumentStatusLog`.`statusID`, ".
693                        "MAX(`tblDocumentStatusLog`.`statusLogID`) AS `maxLogID` ".
694                        "FROM `tblDocumentStatusLog` ".
695                        "GROUP BY `tblDocumentStatusLog`.`statusID` "; //.
696//                        "ORDER BY `maxLogID`";
697                    $dropStr = "DROP TEMPORARY TABLE IF EXISTS `ttstatid`";
698            }
699            if (!$this->_ttstatid) {
700                if (!$this->getResult($queryStr))
701                    return false;
702                $this->_ttstatid = true;
703            } else {
704                if (is_bool($override) && $override) {
705                    if (!$this->getResult($dropStr))
706                        return false;
707                    if (!$this->getResult($queryStr))
708                        return false;
709                }
710            }
711            return $this->_ttstatid;
712        } elseif (!strcasecmp($tableName, "ttcontentid")) {
713            switch ($this->_driver) {
714                case 'sqlite':
715                    $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttcontentid` AS ".
716                        "SELECT `tblDocumentContent`.`document` AS `document`, ".
717                        "MAX(`tblDocumentContent`.`version`) AS `maxVersion` ".
718                        "FROM `tblDocumentContent` ".
719                        "GROUP BY `tblDocumentContent`.`document` ".
720                        "ORDER BY `tblDocumentContent`.`document`";
721                    $dropStr = "DROP TABLE IF EXISTS `ttcontentid`";
722                    break;
723                case 'pgsql':
724                    $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttcontentid` (`document` INTEGER, `maxVersion` INTEGER, PRIMARY KEY (`document`)); ".
725                        "INSERT INTO `ttcontentid` SELECT `tblDocumentContent`.`document` AS `document`, ".
726                        "MAX(`tblDocumentContent`.`version`) AS `maxVersion` ".
727                        "FROM `tblDocumentContent` ".
728                        "GROUP BY `tblDocumentContent`.`document` ".
729                        "ORDER BY `tblDocumentContent`.`document`";
730                    $dropStr = "DROP TABLE IF EXISTS `ttcontentid`";
731                    break;
732                default:
733                    $queryStr = "CREATE TEMPORARY TABLE IF NOT EXISTS `ttcontentid` (PRIMARY KEY (`document`), INDEX (`maxVersion`)) ".
734                        "SELECT `tblDocumentContent`.`document`, ".
735                        "MAX(`tblDocumentContent`.`version`) AS `maxVersion` ".
736                        "FROM `tblDocumentContent` ".
737                        "GROUP BY `tblDocumentContent`.`document` ".
738                        "ORDER BY `tblDocumentContent`.`document`";
739                    $dropStr = "DROP TEMPORARY TABLE IF EXISTS `ttcontentid`";
740            }
741            if (!$this->_ttcontentid) {
742                if (!$this->getResult($queryStr))
743                    return false;
744                $this->_ttcontentid = true;
745            } else {
746                if (is_bool($override) && $override) {
747                    if (!$this->getResult($dropStr))
748                        return false;
749                    if (!$this->getResult($queryStr))
750                        return false;
751                }
752            }
753            return $this->_ttcontentid;
754        }
755        return false;
756    } /* }}} */
757
758    /**
759     * Drop various temporary tables to enforce recreation when needed
760     *
761     * @param string $tableName
762     *
763     * @return bool
764     */
765    private function __dropTemporaryTable($tableName) { /* {{{ */
766        $queryStr = '';
767        if ($this->_driver == 'sqlite' || $this->_driver == 'pgsql') {
768            $t = '';
769        } else {
770            $t = 'TEMPORARY';
771        }
772        if (!strcasecmp($tableName, "ttreviewid")) {
773            $queryStr = "DROP ".$t." TABLE IF EXISTS `ttreviewid`";
774        } elseif (!strcasecmp($tableName, "ttapproveid")) {
775            $queryStr = "DROP ".$t." TABLE IF EXISTS `ttapproveid`";
776        } elseif (!strcasecmp($tableName, "ttstatid")) {
777            $queryStr = "DROP ".$t." TABLE IF EXISTS `ttstatid`";
778        } elseif (!strcasecmp($tableName, "ttcontentid")) {
779            $queryStr = "DROP ".$t." TABLE IF EXISTS `ttcontentid`";
780        }
781        if ($queryStr) {
782            if (!$this->getResult($queryStr)) {
783                return false;
784            } else {
785                $this->{'_'.$tableName} = false;
786                return true;
787            }
788        }
789        return false;
790    } /* }}} */
791
792    /**
793     * Create various views to speed up and simplify sql queries
794     *
795     * @param string $tableName
796     * @param bool $override
797     *
798     * @return bool
799     */
800    private function __createView($tableName, $override = false) { /* {{{ */
801        if (!strcasecmp($tableName, "ttreviewid")) {
802            switch ($this->_driver) {
803                case 'sqlite':
804                    $queryStr = "CREATE VIEW IF NOT EXISTS `ttreviewid` AS ".
805                        "SELECT `tblDocumentReviewLog`.`reviewID` AS `reviewID`, ".
806                        "MAX(`tblDocumentReviewLog`.`reviewLogID`) AS `maxLogID` ".
807                        "FROM `tblDocumentReviewLog` ".
808                        "GROUP BY `tblDocumentReviewLog`.`reviewID` "; //.
809                break;
810                case 'pgsql':
811                    $queryStr = "CREATE VIEW `ttreviewid` AS ".
812                        "SELECT `tblDocumentReviewLog`.`reviewID` AS `reviewID`, ".
813                        "MAX(`tblDocumentReviewLog`.`reviewLogID`) AS `maxLogID` ".
814                        "FROM `tblDocumentReviewLog` ".
815                        "GROUP BY `tblDocumentReviewLog`.`reviewID` ";
816                break;
817                default:
818                    $queryStr = "CREATE".($override ? " OR REPLACE" : "")." VIEW `ttreviewid` AS ".
819                        "SELECT `tblDocumentReviewLog`.`reviewID` AS `reviewID`, ".
820                        "MAX(`tblDocumentReviewLog`.`reviewLogID`) AS `maxLogID` ".
821                        "FROM `tblDocumentReviewLog` ".
822                        "GROUP BY `tblDocumentReviewLog`.`reviewID` ";
823            }
824            if (!$this->_ttreviewid) {
825                if (!$this->getResult($queryStr))
826                    return false;
827                $this->_ttreviewid = true;
828            } else {
829                if (is_bool($override) && $override) {
830//                    if (!$this->getResult("DROP VIEW `ttreviewid`"))
831//                        return false;
832                    if (!$this->getResult($queryStr))
833                        return false;
834                }
835            }
836            return $this->_ttreviewid;
837        } elseif (!strcasecmp($tableName, "ttapproveid")) {
838            switch ($this->_driver) {
839                case 'sqlite':
840                    $queryStr = "CREATE VIEW IF NOT EXISTS `ttapproveid` AS ".
841                        "SELECT `tblDocumentApproveLog`.`approveID` AS `approveID`, ".
842                        "MAX(`tblDocumentApproveLog`.`approveLogID`) AS `maxLogID` ".
843                        "FROM `tblDocumentApproveLog` ".
844                        "GROUP BY `tblDocumentApproveLog`.`approveID` "; //.
845                    break;
846                case 'pgsql':
847                    $queryStr = "CREATE VIEW `ttapproveid` AS ".
848                        "SELECT `tblDocumentApproveLog`.`approveID` AS `approveID`, ".
849                        "MAX(`tblDocumentApproveLog`.`approveLogID`) AS `maxLogID` ".
850                        "FROM `tblDocumentApproveLog` ".
851                        "GROUP BY `tblDocumentApproveLog`.`approveID` ";
852                    break;
853                default:
854                    $queryStr = "CREATE".($override ? " OR REPLACE" : "")." VIEW `ttapproveid` AS ".
855                        "SELECT `tblDocumentApproveLog`.`approveID`, ".
856                        "MAX(`tblDocumentApproveLog`.`approveLogID`) AS `maxLogID` ".
857                        "FROM `tblDocumentApproveLog` ".
858                        "GROUP BY `tblDocumentApproveLog`.`approveID` ";
859            }
860            if (!$this->_ttapproveid) {
861                if (!$this->getResult($queryStr))
862                    return false;
863                $this->_ttapproveid = true;
864            } else {
865                if (is_bool($override) && $override) {
866//                    if (!$this->getResult("DROP VIEW `ttapproveid`"))
867//                        return false;
868                    if (!$this->getResult($queryStr))
869                        return false;
870                }
871            }
872            return $this->_ttapproveid;
873        } elseif (!strcasecmp($tableName, "ttstatid")) {
874            switch ($this->_driver) {
875                case 'sqlite':
876                    $queryStr = "CREATE VIEW IF NOT EXISTS `ttstatid` AS ".
877                        "SELECT `tblDocumentStatusLog`.`statusID` AS `statusID`, ".
878                        "MAX(`tblDocumentStatusLog`.`statusLogID`) AS `maxLogID` ".
879                        "FROM `tblDocumentStatusLog` ".
880                        "GROUP BY `tblDocumentStatusLog`.`statusID` ";
881                    break;
882                case 'pgsql':
883                    $queryStr = "CREATE VIEW `ttstatid` AS ".
884                        "SELECT `tblDocumentStatusLog`.`statusID` AS `statusID`, ".
885                        "MAX(`tblDocumentStatusLog`.`statusLogID`) AS `maxLogID` ".
886                        "FROM `tblDocumentStatusLog` ".
887                        "GROUP BY `tblDocumentStatusLog`.`statusID` ";
888                    break;
889                default:
890                    $queryStr = "CREATE".($override ? " OR REPLACE" : "")." VIEW `ttstatid` AS ".
891                        "SELECT `tblDocumentStatusLog`.`statusID`, ".
892                        "MAX(`tblDocumentStatusLog`.`statusLogID`) AS `maxLogID` ".
893                        "FROM `tblDocumentStatusLog` ".
894                        "GROUP BY `tblDocumentStatusLog`.`statusID` ";
895            }
896            if (!$this->_ttstatid) {
897                if (!$this->getResult($queryStr))
898                    return false;
899                $this->_ttstatid = true;
900            } else {
901                if (is_bool($override) && $override) {
902//                    if (!$this->getResult("DROP VIEW `ttstatid`"))
903//                        return false;
904                    if (!$this->getResult($queryStr))
905                        return false;
906                }
907            }
908            return $this->_ttstatid;
909        } elseif (!strcasecmp($tableName, "ttcontentid")) {
910            switch ($this->_driver) {
911                case 'sqlite':
912                    $queryStr = "CREATE VIEW IF NOT EXISTS `ttcontentid` AS ".
913                        "SELECT `tblDocumentContent`.`document` AS `document`, ".
914                        "MAX(`tblDocumentContent`.`version`) AS `maxVersion` ".
915                        "FROM `tblDocumentContent` ".
916                        "GROUP BY `tblDocumentContent`.`document` ".
917                        "ORDER BY `tblDocumentContent`.`document`";
918                    break;
919                case 'pgsql':
920                    $queryStr = "CREATE VIEW `ttcontentid` AS ".
921                        "SELECT `tblDocumentContent`.`document` AS `document`, ".
922                        "MAX(`tblDocumentContent`.`version`) AS `maxVersion` ".
923                        "FROM `tblDocumentContent` ".
924                        "GROUP BY `tblDocumentContent`.`document` ".
925                        "ORDER BY `tblDocumentContent`.`document`";
926                    break;
927                default:
928                    $queryStr = "CREATE".($override ? " OR REPLACE" : "")." VIEW `ttcontentid` AS ".
929                        "SELECT `tblDocumentContent`.`document`, ".
930                        "MAX(`tblDocumentContent`.`version`) AS `maxVersion` ".
931                        "FROM `tblDocumentContent` ".
932                        "GROUP BY `tblDocumentContent`.`document` ".
933                        "ORDER BY `tblDocumentContent`.`document`";
934            }
935            if (!$this->_ttcontentid) {
936                if (!$this->getResult($queryStr))
937                    return false;
938                $this->_ttcontentid = true;
939            } else {
940                if (is_bool($override) && $override) {
941//                    if (!$this->getResult("DROP VIEW `ttcontentid`"))
942//                        return false;
943                    if (!$this->getResult($queryStr))
944                        return false;
945                }
946            }
947            return $this->_ttcontentid;
948        }
949        return false;
950    } /* }}} */
951
952    /**
953     * Create various temporary tables or view to speed up and simplify sql queries
954     *
955     * @param string $tableName
956     * @param bool $override
957     *
958     * @return bool
959     */
960    public function createTemporaryTable($tableName, $override = false) { /* {{{ */
961        if ($this->_useviews) {
962            return $this->__createView($tableName, $override);
963        } else {
964            return $this->__createTemporaryTable($tableName, $override);
965        }
966    } /* }}} */
967
968    /**
969     * Drop various temporary tables to force recreation when next time needed
970     *
971     * @param string $tableName
972     *
973     * @return bool
974     */
975    public function dropTemporaryTable($tableName) { /* {{{ */
976        if ($this->_useviews) {
977            return true; // No need to recreate a view
978        } else {
979            return $this->__dropTemporaryTable($tableName);
980        }
981    } /* }}} */
982
983    /**
984     * Return sql statement for extracting the date part from a field
985     * containing a unix timestamp
986     *
987     * @param string $fieldname name of field containing the timestamp
988     * @param string $format
989     * @return string sql code
990     */
991    public function getDateExtract($fieldname, $format = '%Y-%m-%d') { /* {{{ */
992        switch ($this->_driver) {
993            case 'mysql':
994                return "from_unixtime(`".$fieldname."`, ".$this->qstr($format).")";
995                break;
996            case 'sqlite':
997                return "strftime(".$this->qstr($format).", `".$fieldname."`, 'unixepoch')";
998                break;
999            case 'pgsql':
1000                switch ($format) {
1001                case '%Y-%m':
1002                    return "to_char(to_timestamp(`".$fieldname."`), 'YYYY-MM')";
1003                    break;
1004                default:
1005                    return "to_char(to_timestamp(`".$fieldname."`), 'YYYY-MM-DD')";
1006                    break;
1007                }
1008                break;
1009        }
1010        return '';
1011    } /* }}} */
1012
1013    /**
1014     * Return sql statement for returning the current date and time
1015     * in format Y-m-d H:i:s
1016     *
1017     * @return string sql code
1018     */
1019    public function getCurrentDatetime($offset = 0) { /* {{{ */
1020        switch ($this->_driver) {
1021        case 'mysql':
1022            if($offset) {
1023                return "CURRENT_TIMESTAMP + INTERVAL ".((int) $offset)." SECOND";
1024            } else {
1025                return "CURRENT_TIMESTAMP";
1026            }
1027            break;
1028        case 'sqlite':
1029            if($offset) {
1030                return "datetime('now', 'localtime', '".((int) $offset)." seconds')";
1031            } else {
1032                return "datetime('now', 'localtime')";
1033            }
1034            break;
1035        case 'pgsql':
1036            if($offset) {
1037                return "now() + INTERVAL ".((int) $offset)." SECOND";
1038            } else {
1039                return "now()";
1040            }
1041            break;
1042        }
1043        return '';
1044    } /* }}} */
1045
1046    /**
1047     * Return sql statement for returning the current timestamp
1048     *
1049     * @return string sql code
1050     */
1051    public function getCurrentTimestamp() { /* {{{ */
1052        switch ($this->_driver) {
1053            case 'mysql':
1054                return "UNIX_TIMESTAMP()";
1055                break;
1056            case 'sqlite':
1057                return "strftime('%s', 'now')";
1058                break;
1059            case 'pgsql':
1060                return "date_part('epoch',CURRENT_TIMESTAMP)::int";
1061                break;
1062        }
1063        return '';
1064    } /* }}} */
1065
1066    /**
1067     * Return sql statement for returning the current timestamp
1068     *
1069     * @param $field
1070     * @return string sql code
1071     */
1072    public function castToText($field) { /* {{{ */
1073        switch ($this->_driver) {
1074            case 'pgsql':
1075                return $field."::TEXT";
1076                break;
1077        }
1078        return $field;
1079    } /* }}} */
1080
1081    /**
1082     * Create an sql dump of the complete database
1083     *
1084     * @param resource $fp name of dump file
1085     * @return bool
1086     */
1087    public function createDump($fp) { /* {{{ */
1088        $tables = $this->TableList('TABLES');
1089        foreach ($tables as $table) {
1090            if ($table == 'sqlite_sequence')
1091                continue;
1092            $query = "SELECT * FROM `".$table."`";
1093            $records = $this->getResultArray($query);
1094            fwrite($fp, "\n-- TABLE: ".$table."--\n\n");
1095            foreach ($records as $record) {
1096                $values = "";
1097                $i = 1;
1098                foreach ($record as $column) {
1099                    if (is_null($column)) $values .= 'NULL';
1100                    elseif (is_numeric($column)) $values .= $column;
1101                    else $values .= $this->qstr($column);
1102
1103                    if ($i<(count($record))) $values .= ",";
1104                    $i++;
1105                }
1106
1107                fwrite($fp, "INSERT INTO `".$table."` VALUES (".$values.");\n");
1108            }
1109        }
1110        return true;
1111    } /* }}} */
1112
1113    /**
1114     * Run a list of sql queries
1115     *
1116     * @param array $queries list of sql queries
1117     * @param bool $transaction enclose all queries into a transaction
1118     * @return array list of error messages. If no error msgs are returned
1119     * then all queries very successful.
1120     */
1121    public function runQueries(array $queries, bool $transaction=false) { /* {{{ */
1122        $errorMsg = [];
1123        if ($transaction)
1124            $this->startTransaction();
1125        foreach ($queries as $query) {
1126            $query = trim($query);
1127            if (!empty($query)) {
1128                if (false === $this->getResult($query)) {
1129                    $errorMsg[] = [$query, $this->getErrorMsg()] ;
1130                    if ($transaction) {
1131                        $this->rollbackTransaction();
1132                        return $errorMsg;
1133                    }
1134                }
1135            }
1136        }
1137        if ($transaction)
1138            $this->commitTransaction();
1139        return $errorMsg;
1140    } /* }}} */
1141
1142    /**
1143     * Run queries from file
1144     *
1145     * This methods executes all queries in a file. It is usually used by
1146     * extensions to create new tables. Queries must be seperated by a
1147     * semicolon.
1148     *
1149     * @param string $file name of file containing queries
1150     * @return array list of error msg
1151     */
1152    public function runQueriesFromFile($file) { /* {{{ */
1153        $errorMsg = [];
1154        if ($queries = file_get_contents($file)) {
1155            $queries = explode(";", $queries);
1156            // execute queries
1157            if ($queries) {
1158                $errorMsg = $this->runQueries($queries);
1159            }
1160        }
1161        return $errorMsg;
1162    } /* }}} */
1163}