Completed
Push — master ( 1ba1d1...f06abd )
by Ivan
03:09
created

Driver::tables()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace vakata\database\driver\postgre;
4
5
use \vakata\database\DBException;
6
use \vakata\database\DriverInterface;
7
use \vakata\database\DriverAbstract;
8
use \vakata\database\StatementInterface;
9
use \vakata\database\schema\Table;
10
use \vakata\database\schema\TableRelation;
11
use \vakata\collection\Collection;
12
13
class Driver extends DriverAbstract implements DriverInterface
14
{
15
    protected $lnk = null;
16
    protected $transaction = false;
17
18
    public function __construct(array $connection)
19
    {
20
        $this->connection = $connection;
21
    }
22
    public function __destruct()
23
    {
24
        $this->disconnect();
25
    }
26
    protected function connect()
27
    {
28
        if ($this->lnk === null) {
29
            $this->lnk = call_user_func(
30
                $this->option('persist') ? '\pg_pconnect' : '\pg_connect',
31
                implode(" ", [
32
                    'user='.$this->connection['user'],
33
                    'password='.$this->connection['pass'],
34
                    'host='.$this->connection['host'],
35
                    'dbname='.$this->connection['name'],
36
                    "options='--client_encoding=".$this->option('charset', 'utf8')."'"
37
                ])
38
            );
39
            if ($this->lnk === false) {
40
                throw new DBException('Connect error');
41
            }
42
        }
43
    }
44
    public function test() : bool
45
    {
46
        if ($this->lnk) {
47
            return true;
48
        }
49
        try {
50
            @$this->connect();
51
            return true;
52
        } catch (\Exception $e) {
53
            $this->lnk = null;
54
            return false;
55
        }
56
    }
57
    protected function disconnect()
58
    {
59
        if (is_resource($this->lnk)) {
60
            \pg_close($this->lnk);
61
        }
62
    }
63
    public function prepare(string $sql) : StatementInterface
64
    {
65
        $this->connect();
66
        $binder = '?';
67
        if (strpos($sql, $binder) !== false) {
68
            $tmp = explode($binder, $sql);
69
            $sql = '';
70
            foreach ($tmp as $i => $v) {
71
                $sql .= $v;
72
                if (isset($tmp[($i + 1)])) {
73
                    $sql .= '$'.($i + 1);
74
                }
75
            }
76
        }
77
        return new Statement($sql, $this->lnk);
78
    }
79
80
    public function begin() : bool
81
    {
82
        $this->connect();
83
        try {
84
            $this->transaction = true;
85
            $this->query('BEGIN');
86
        } catch (DBException $e) {
87
            $this->transaction = false;
88
89
            return false;
90
        }
91
92
        return true;
93
    }
94
    public function commit() : bool
95
    {
96
        $this->connect();
97
        $this->transaction = false;
98
        try {
99
            $this->query('COMMIT');
100
        } catch (DBException $e) {
101
            return false;
102
        }
103
104
        return true;
105
    }
106
    public function rollback() : bool
107
    {
108
        $this->connect();
109
        $this->transaction = false;
110
        try {
111
            $this->query('ROLLBACK');
112
        } catch (DBException $e) {
113
            return false;
114
        }
115
116
        return true;
117
    }
118
    public function isTransaction()
119
    {
120
        return $this->transaction;
121
    }
122
    public function table(string $table, bool $detectRelations = true) : Table
123
    {
124
        static $tables = [];
125
        if (isset($tables[$table])) {
126
            return $tables[$table];
127
        }
128
129
        static $relationsT = null;
130
        static $relationsR = null;
131
        if (!isset($relationsT) || !isset($relationsR)) {
132
            $relationsT = [];
133
            $relationsR = [];
134
            $col = Collection::from(
135
                $this->query(
136
                    "SELECT
137
                        kc.table_name,
138
                        kc.column_name,
139
                        kc.constraint_name,
140
                        ct.table_name AS referenced_table_name,
141
                        (SELECT column_name
142
                         FROM information_schema.constraint_column_usage
143
                         WHERE constraint_name = kc.constraint_name AND table_name = ct.table_name
144
                         LIMIT 1 OFFSET kc.position_in_unique_constraint - 1
145
                        ) AS referenced_column_name
146
                     FROM information_schema.key_column_usage kc
147
                     JOIN information_schema.constraint_table_usage ct ON kc.constraint_name = ct.constraint_name AND ct.table_schema = kc.table_schema
148
                     WHERE
149
                        kc.table_schema = ? AND kc.table_name IS NOT NULL AND kc.position_in_unique_constraint IS NOT NULL",
150
                    [ $this->connection['opts']['schema'] ?? $this->connection['name'] ]
151
                )
152
            )->toArray();
153
            foreach ($col as $row) {
154
                $relationsT[$row['table_name']][] = $row;
155
                $relationsR[$row['referenced_table_name']][] = $row;
156
            }
157
        }
158
159
        $columns = Collection::from($this
160
            ->query(
161
                "SELECT * FROM information_schema.columns WHERE table_name = ? AND table_schema = ?",
162
                [ $table, $this->connection['opts']['schema'] ?? $this->connection['name'] ]
163
            ))
164
            ->mapKey(function ($v) { return $v['column_name']; })
165
            ->map(function ($v) {
166
                $v['length'] = null;
167
                if (!isset($v['data_type'])) {
168
                    return $v;
169
                }
170
                switch ($v['data_type']) {
171
                    case 'character':
172
                    case 'character varying':
173
                        $v['length'] = (int)$v['character_maximum_length'];
174
                        break;
175
                }
176
                return $v;
177
            })
178
            ->toArray();
179
        if (!count($columns)) {
180
            throw new DBException('Table not found by name');
181
        }
182
        $pkname = Collection::from($this
183
            ->query(
184
                "SELECT constraint_name FROM information_schema.table_constraints
185
                WHERE table_name = ? AND constraint_type = ? AND table_schema = ?",
186
                [ $table, 'PRIMARY KEY', $this->connection['opts']['schema'] ?? $this->connection['name'] ]
187
            ))
188
            ->pluck('constraint_name')
189
            ->value();
190
        $primary = [];
191
        if ($pkname) {
192
            $primary = Collection::from($this
193
                ->query(
194
                    "SELECT column_name FROM information_schema.constraint_column_usage
195
                     WHERE table_name = ? AND constraint_name = ? AND table_schema = ?",
196
                    [ $table, $pkname, $this->connection['opts']['schema'] ?? $this->connection['name'] ]
197
                ))
198
                ->pluck('column_name')
199
                ->toArray();
200
        }
201
        $tables[$table] = $definition = (new Table($table))
202
            ->addColumns($columns)
203
            ->setPrimaryKey($primary)
204
            ->setComment('');
205
206
        if ($detectRelations) {
207
            // relations where the current table is referenced
208
            // assuming current table is on the "one" end having "many" records in the referencing table
209
            // resulting in a "hasMany" or "manyToMany" relationship (if a pivot table is detected)
210
            $relations = [];
211
            foreach ($relationsR[$table] ?? [] as $relation) {
212
                $relations[$relation['constraint_name']]['table'] = $relation['table_name'];
213
                $relations[$relation['constraint_name']]['keymap'][$relation['referenced_column_name']] = $relation['column_name'];
214
            }
215
            foreach ($relations as $data) {
216
                $rtable = $this->table($data['table'], true);
217
                $columns = [];
218
                foreach ($rtable->getColumns() as $column) {
219
                    if (!in_array($column, $data['keymap'])) {
220
                        $columns[] = $column;
221
                    }
222
                }
223
                $foreign = [];
224
                $usedcol = [];
225
                if (count($columns)) {
226
                    foreach (Collection::from($relationsT[$data['table']] ?? [])
227
                        ->filter(function ($v) use ($columns) {
228
                            return in_array($v['column_name'], $columns);
229
                        }) as $relation
230
                    ) {
231
                        $foreign[$relation['constraint_name']]['table'] = $relation['referenced_table_name'];
232
                        $foreign[$relation['constraint_name']]['keymap'][$relation['column_name']] = $relation['referenced_column_name'];
233
                        $usedcol[] = $relation['column_name'];
234
                    }
235
                }
236
                if (count($foreign) === 1 && !count(array_diff($columns, $usedcol))) {
237
                    $foreign = current($foreign);
238
                    $relname = $foreign['table'];
239
                    $cntr = 1;
240
                    while ($definition->hasRelation($relname) || $definition->getName() == $relname) {
241
                        $relname = $foreign['table'] . '_' . (++ $cntr);
242
                    }
243
                    $definition->addRelation(
244
                        new TableRelation(
245
                            $relname,
246
                            $this->table($foreign['table'], true),
247
                            $data['keymap'],
248
                            true,
249
                            $rtable,
250
                            $foreign['keymap']
251
                        )
252
                    );
253
                } else {
254
                    $relname = $data['table'];
255
                    $cntr = 1;
256
                    while ($definition->hasRelation($relname) || $definition->getName() == $relname) {
257
                        $relname = $data['table'] . '_' . (++ $cntr);
258
                    }
259
                    $definition->addRelation(
260
                        new TableRelation(
261
                            $relname,
262
                            $this->table($data['table'], true),
263
                            $data['keymap'],
264
                            true
265
                        )
266
                    );
267
                }
268
            }
269
            // relations where the current table references another table
270
            // assuming current table is linked to "one" record in the referenced table
271
            // resulting in a "belongsTo" relationship
272
            $relations = [];
273
            foreach ($relationsT[$table] ?? [] as $relation) {
274
                $relations[$relation['constraint_name']]['table'] = $relation['referenced_table_name'];
275
                $relations[$relation['constraint_name']]['keymap'][$relation['column_name']] = $relation['referenced_column_name'];
276
            }
277
            foreach ($relations as $name => $data) {
278
                $relname = $data['table'];
279
                $cntr = 1;
280
                while ($definition->hasRelation($relname) || $definition->getName() == $relname) {
281
                    $relname = $data['table'] . '_' . (++ $cntr);
282
                }
283
                $definition->addRelation(
284
                    new TableRelation(
285
                        $relname,
286
                        $this->table($data['table'], true),
287
                        $data['keymap'],
288
                        false
289
                    )
290
                );
291
            }
292
        }
293
        return $definition->toLowerCase();
294
    }
295
    public function tables() : array
296
    {
297
        return Collection::from($this
298
            ->query(
299
                "SELECT table_name FROM information_schema.tables where table_schema = ?",
300
                [ $this->connection['opts']['schema'] ?? $this->connection['name'] ]
301
            ))
302
            ->pluck('table_name')
303
            ->map(function ($v) {
304
                return $this->table($v);
305
            })
306
            ->toArray();
307
    }
308
}
309