Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 135
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
postgres_extractor
0.00% covered (danger)
0.00%
0 / 135
0.00% covered (danger)
0.00%
0 / 4
1560
0.00% covered (danger)
0.00%
0 / 1
 write_start
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 write_table
0.00% covered (danger)
0.00%
0 / 89
0.00% covered (danger)
0.00%
0 / 1
650
 write_data
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
110
 write_end
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14namespace phpbb\db\extractor;
15
16use phpbb\db\extractor\exception\extractor_not_initialized_exception;
17
18class postgres_extractor extends base_extractor
19{
20    /**
21    * {@inheritdoc}
22    */
23    public function write_start($table_prefix)
24    {
25        if (!$this->is_initialized)
26        {
27            throw new extractor_not_initialized_exception();
28        }
29
30        $sql_data = "--\n";
31        $sql_data .= "-- phpBB Backup Script\n";
32        $sql_data .= "-- Dump of tables for $table_prefix\n";
33        $sql_data .= "-- DATE : " . gmdate("d-m-Y H:i:s", $this->time) . " GMT\n";
34        $sql_data .= "--\n";
35        $sql_data .= "BEGIN TRANSACTION;\n";
36        $this->flush($sql_data);
37    }
38
39    /**
40    * {@inheritdoc}
41    */
42    public function write_table($table_name)
43    {
44        static $domains_created = array();
45
46        if (!$this->is_initialized)
47        {
48            throw new extractor_not_initialized_exception();
49        }
50
51        $sql = "SELECT a.domain_name, a.data_type, a.character_maximum_length, a.domain_default
52            FROM INFORMATION_SCHEMA.domains a, INFORMATION_SCHEMA.column_domain_usage b
53            WHERE a.domain_name = b.domain_name
54                AND b.table_name = '{$table_name}'";
55        $result = $this->db->sql_query($sql);
56        while ($row = $this->db->sql_fetchrow($result))
57        {
58            if (empty($domains_created[$row['domain_name']]))
59            {
60                $domains_created[$row['domain_name']] = true;
61                //$sql_data = "DROP DOMAIN {$row['domain_name']};\n";
62                $sql_data = "CREATE DOMAIN {$row['domain_name']} as {$row['data_type']}";
63                if (!empty($row['character_maximum_length']))
64                {
65                    $sql_data .= '(' . $row['character_maximum_length'] . ')';
66                }
67                $sql_data .= ' NOT NULL';
68                if (!empty($row['domain_default']))
69                {
70                    $sql_data .= ' DEFAULT ' . $row['domain_default'];
71                }
72                $this->flush($sql_data . ";\n");
73            }
74        }
75        $this->db->sql_freeresult($result);
76
77        $sql_data = '-- Table: ' . $table_name . "\n";
78        $sql_data .= "DROP TABLE $table_name;\n";
79        // PGSQL does not "tightly" bind sequences and tables, we must guess...
80        $sql = "SELECT relname
81            FROM pg_class
82            WHERE relkind = 'S'
83                AND relname = '{$table_name}_seq'";
84        $result = $this->db->sql_query($sql);
85        // We don't even care about storing the results. We already know the answer if we get rows back.
86        if ($this->db->sql_fetchrow($result))
87        {
88            $sql_data .= "DROP SEQUENCE IF EXISTS {$table_name}_seq;\n";
89            $sql_data .= "CREATE SEQUENCE {$table_name}_seq;\n";
90        }
91        $this->db->sql_freeresult($result);
92
93        $field_query = "SELECT a.attnum, a.attname as field, t.typname as type, a.attlen as length, a.atttypmod as lengthvar, a.attnotnull as notnull
94            FROM pg_class c, pg_attribute a, pg_type t
95            WHERE c.relname = '" . $this->db->sql_escape($table_name) . "'
96                AND a.attnum > 0
97                AND a.attrelid = c.oid
98                AND a.atttypid = t.oid
99            ORDER BY a.attnum";
100        $result = $this->db->sql_query($field_query);
101
102        $sql_data .= "CREATE TABLE $table_name(\n";
103        $lines = array();
104        while ($row = $this->db->sql_fetchrow($result))
105        {
106            // Get the data from the table
107            $sql_get_default = "SELECT pg_get_expr(d.adbin, d.adrelid) as rowdefault
108                FROM pg_attrdef d, pg_class c
109                WHERE (c.relname = '" . $this->db->sql_escape($table_name) . "')
110                    AND (c.oid = d.adrelid)
111                    AND d.adnum = " . $row['attnum'];
112            $def_res = $this->db->sql_query($sql_get_default);
113            $def_row = $this->db->sql_fetchrow($def_res);
114            $this->db->sql_freeresult($def_res);
115
116            if (empty($def_row))
117            {
118                unset($row['rowdefault']);
119            }
120            else
121            {
122                $row['rowdefault'] = $def_row['rowdefault'];
123            }
124
125            if ($row['type'] == 'bpchar')
126            {
127                // Internally stored as bpchar, but isn't accepted in a CREATE TABLE statement.
128                $row['type'] = 'char';
129            }
130
131            $line = '  ' . $row['field'] . ' ' . $row['type'];
132
133            if (strpos($row['type'], 'char') !== false)
134            {
135                if ($row['lengthvar'] > 0)
136                {
137                    $line .= '(' . ($row['lengthvar'] - 4) . ')';
138                }
139            }
140
141            if (strpos($row['type'], 'numeric') !== false)
142            {
143                $line .= '(';
144                $line .= sprintf("%s,%s", (($row['lengthvar'] >> 16) & 0xffff), (($row['lengthvar'] - 4) & 0xffff));
145                $line .= ')';
146            }
147
148            if (isset($row['rowdefault']))
149            {
150                $line .= ' DEFAULT ' . $row['rowdefault'];
151            }
152
153            if ($row['notnull'] == 't')
154            {
155                $line .= ' NOT NULL';
156            }
157
158            $lines[] = $line;
159        }
160        $this->db->sql_freeresult($result);
161
162        // Get the listing of primary keys.
163        $sql_pri_keys = "SELECT ic.relname as index_name, bc.relname as tab_name, ta.attname as column_name, i.indisunique as unique_key, i.indisprimary as primary_key
164            FROM pg_class bc, pg_class ic, pg_index i, pg_attribute ta, pg_attribute ia
165            WHERE (bc.oid = i.indrelid)
166                AND (ic.oid = i.indexrelid)
167                AND (ia.attrelid = i.indexrelid)
168                AND    (ta.attrelid = bc.oid)
169                AND (bc.relname = '" . $this->db->sql_escape($table_name) . "')
170                AND (ta.attrelid = i.indrelid)
171                AND (ta.attnum = i.indkey[ia.attnum-1])
172            ORDER BY index_name, tab_name, column_name";
173
174        $result = $this->db->sql_query($sql_pri_keys);
175
176        $index_create = $index_rows = $primary_key = array();
177
178        // We do this in two steps. It makes placing the comma easier
179        while ($row = $this->db->sql_fetchrow($result))
180        {
181            if ($row['primary_key'] == 't')
182            {
183                $primary_key[] = $row['column_name'];
184                $primary_key_name = $row['index_name'];
185            }
186            else
187            {
188                // We have to store this all this info because it is possible to have a multi-column key...
189                // we can loop through it again and build the statement
190                $index_rows[$row['index_name']]['table'] = $table_name;
191                $index_rows[$row['index_name']]['unique'] = ($row['unique_key'] == 't') ? true : false;
192                $index_rows[$row['index_name']]['column_names'][] = $row['column_name'];
193            }
194        }
195        $this->db->sql_freeresult($result);
196
197        if (!empty($index_rows))
198        {
199            foreach ($index_rows as $idx_name => $props)
200            {
201                $index_create[] = 'CREATE ' . ($props['unique'] ? 'UNIQUE ' : '') . "INDEX $idx_name ON $table_name (" . implode(', ', $props['column_names']) . ");";
202            }
203        }
204
205        if (!empty($primary_key))
206        {
207            $lines[] = "  CONSTRAINT $primary_key_name PRIMARY KEY (" . implode(', ', $primary_key) . ")";
208        }
209
210        // Generate constraint clauses for CHECK constraints
211        $sql_checks = "SELECT pc.conname AS index_name, pg_get_expr(pc.conbin, pc.conrelid) AS constraint_expr
212            FROM pg_constraint pc, pg_class bc
213            WHERE pc.conrelid = bc.oid
214                AND bc.relname = '" . $this->db->sql_escape($table_name) . "'
215                AND NOT EXISTS (
216                    SELECT *
217                    FROM pg_constraint AS c, pg_inherits AS i
218                        WHERE i.inhrelid = pc.conrelid
219                            AND c.conname = pc.conname
220                            AND pg_get_constraintdef(c.oid) = pg_get_constraintdef(pc.oid)
221                            AND c.conrelid = i.inhparent
222                )";
223        $result = $this->db->sql_query($sql_checks);
224
225        // Add the constraints to the sql file.
226        while ($row = $this->db->sql_fetchrow($result))
227        {
228            if (!empty($row['constraint_expr']))
229            {
230                $lines[] = '  CONSTRAINT ' . $row['index_name'] . ' CHECK ' . $row['constraint_expr'];
231            }
232        }
233        $this->db->sql_freeresult($result);
234
235        $sql_data .= implode(", \n", $lines);
236        $sql_data .= "\n);\n";
237
238        if (!empty($index_create))
239        {
240            $sql_data .= implode("\n", $index_create) . "\n\n";
241        }
242        $this->flush($sql_data);
243    }
244
245    /**
246    * {@inheritdoc}
247    */
248    public function write_data($table_name)
249    {
250        if (!$this->is_initialized)
251        {
252            throw new extractor_not_initialized_exception();
253        }
254
255        // Grab all of the data from current table.
256        $sql = "SELECT *
257            FROM $table_name";
258        $result = $this->db->sql_query($sql);
259
260        $i_num_fields = pg_num_fields($result);
261        $seq = '';
262
263        for ($i = 0; $i < $i_num_fields; $i++)
264        {
265            $ary_type[] = pg_field_type($result, $i);
266            $ary_name[] = pg_field_name($result, $i);
267
268            $sql = "SELECT pg_get_expr(d.adbin, d.adrelid) as rowdefault
269                FROM pg_attrdef d, pg_class c
270                WHERE (c.relname = '{$table_name}')
271                    AND (c.oid = d.adrelid)
272                    AND d.adnum = " . strval($i + 1);
273            $result2 = $this->db->sql_query($sql);
274            if ($row = $this->db->sql_fetchrow($result2))
275            {
276                // Determine if we must reset the sequences
277                if (strpos($row['rowdefault'], "nextval('") === 0)
278                {
279                    $seq .= "SELECT SETVAL('{$table_name}_seq',(select case when max({$ary_name[$i]})>0 then max({$ary_name[$i]})+1 else 1 end FROM {$table_name}));\n";
280                }
281            }
282        }
283
284        $this->flush("COPY $table_name (" . implode(', ', $ary_name) . ') FROM stdin;' . "\n");
285        while ($row = $this->db->sql_fetchrow($result))
286        {
287            $schema_vals = array();
288
289            // Build the SQL statement to recreate the data.
290            for ($i = 0; $i < $i_num_fields; $i++)
291            {
292                $str_val = $row[$ary_name[$i]];
293
294                if (preg_match('#char|text|bool|bytea#i', $ary_type[$i]))
295                {
296                    $str_val = str_replace(array("\n", "\t", "\r", "\b", "\f", "\v"), array('\n', '\t', '\r', '\b', '\f', '\v'), addslashes($str_val));
297                    $str_empty = '';
298                }
299                else
300                {
301                    $str_empty = '\N';
302                }
303
304                if (empty($str_val) && $str_val !== '0')
305                {
306                    $str_val = $str_empty;
307                }
308
309                $schema_vals[] = $str_val;
310            }
311
312            // Take the ordered fields and their associated data and build it
313            // into a valid sql statement to recreate that field in the data.
314            $this->flush(implode("\t", $schema_vals) . "\n");
315        }
316        $this->db->sql_freeresult($result);
317        $this->flush("\\.\n");
318
319        // Write out the sequence statements
320        $this->flush($seq);
321    }
322
323    /**
324    * Writes closing line(s) to database backup
325    *
326    * @return void
327    * @throws extractor_not_initialized_exception when calling this function before init_extractor()
328    */
329    public function write_end()
330    {
331        if (!$this->is_initialized)
332        {
333            throw new extractor_not_initialized_exception();
334        }
335
336        $this->flush("COMMIT;\n");
337        parent::write_end();
338    }
339}