1
|
|
|
<?php |
2
|
|
|
/****************************************************************************** |
3
|
|
|
* An implementation of dicto (scg.unibe.ch/dicto) in and for PHP. |
4
|
|
|
* |
5
|
|
|
* Copyright (c) 2016, 2015 Richard Klees <[email protected]> |
6
|
|
|
* |
7
|
|
|
* This software is licensed under The MIT License. You should have received |
8
|
|
|
* a copy of the license along with the code. |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace Lechimp\Dicto\App; |
12
|
|
|
|
13
|
|
|
use Lechimp\Dicto\Graph; |
14
|
|
|
use Lechimp\Dicto\Indexer\Insert; |
15
|
|
|
use Doctrine\DBAL\Schema; |
16
|
|
|
use Doctrine\DBAL\Types\Type; |
17
|
|
|
use Doctrine\DBAL\Schema\Synchronizer\SingleDatabaseSynchronizer; |
18
|
|
|
use Doctrine\DBAL\Statement; |
19
|
|
|
|
20
|
|
|
class IndexDB extends DB implements Insert { |
21
|
|
|
protected $nodes_per_insert = 200; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @var array |
25
|
|
|
*/ |
26
|
|
|
protected $tables = |
27
|
|
|
[ "files" => ["_file", |
28
|
|
|
["id", "path", "source"]] |
29
|
|
|
, "namespaces" => ["_namespace", |
30
|
|
|
["id", "name"]] |
31
|
|
|
, "classes" => ["_class", |
32
|
|
|
["id", "name", "file_id", "start_line", "end_line", "namespace_id"]] |
33
|
|
|
, "interfaces" => ["_interface", |
34
|
|
|
["id", "name", "file_id", "start_line", "end_line", "namespace_id"]] |
35
|
|
|
, "traits" => ["_trait", |
36
|
|
|
["id", "name", "file_id", "start_line", "end_line", "namespace_id"]] |
37
|
|
|
, "methods" => ["_method", |
38
|
|
|
["id", "name", "class_id", "file_id", "start_line", "end_line"]] |
39
|
|
|
, "functions" => ["_function", |
40
|
|
|
["id", "name", "file_id", "start_line", "end_line", "namespace_id"]] |
41
|
|
|
, "globals" => ["_global", |
42
|
|
|
["id", "name"]] |
43
|
|
|
, "language_constructs" => ["_language_construct", |
44
|
|
|
["id", "name"]] |
45
|
|
|
, "method_references" => ["_method_reference", |
46
|
|
|
["id", "name", "file_id", "line", "column"]] |
47
|
|
|
, "function_references" => ["_function_reference", |
48
|
|
|
["id", "name", "file_id", "line", "column"]] |
49
|
|
|
, "relations" => ["_relation", |
50
|
|
|
["left_id", "relation", "right_id", "file_id", "line"]] |
51
|
|
|
]; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @var array[] |
55
|
|
|
*/ |
56
|
|
|
protected $caches = []; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var integer |
60
|
|
|
*/ |
61
|
|
|
protected $id_counter = 0; |
62
|
|
|
|
63
|
1 |
|
public function __construct($connection) { |
64
|
1 |
|
parent::__construct($connection); |
65
|
1 |
|
foreach ($this->tables as $table => $_) { |
66
|
1 |
|
$this->caches[$table] = []; |
67
|
1 |
|
} |
68
|
1 |
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* @inheritdocs |
72
|
|
|
*/ |
73
|
1 |
|
public function _file($path, $source) { |
74
|
1 |
|
return $this->append_and_maybe_flush("files", |
75
|
1 |
|
[null, $this->esc_str($path), $this->esc_str($source)]); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* @inheritdocs |
80
|
|
|
*/ |
81
|
1 |
|
public function _namespace($name) { |
82
|
1 |
|
return $this->append_and_maybe_flush("namespaces", |
83
|
1 |
|
[null, $this->esc_str($name)]); |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* @inheritdocs |
88
|
|
|
*/ |
89
|
1 |
|
public function _class($name, $file, $start_line, $end_line, $namespace = null) { |
90
|
1 |
|
return $this->append_and_maybe_flush("classes", |
91
|
1 |
|
[null, $this->esc_str($name), $file, $start_line, $end_line, $this->esc_maybe_null($namespace)]); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* @inheritdocs |
96
|
|
|
*/ |
97
|
|
|
public function _interface($name, $file, $start_line, $end_line, $namespace = null) { |
98
|
|
|
return $this->append_and_maybe_flush("interfaces", |
99
|
|
|
[null, $this->esc_str($name), $file, $start_line, $end_line, $this->esc_maybe_null($namespace)]); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* @inheritdocs |
104
|
|
|
*/ |
105
|
|
|
public function _trait($name, $file, $start_line, $end_line, $namespace = null) { |
106
|
|
|
return $this->append_and_maybe_flush("traits", |
107
|
|
|
[null, $this->esc_str($name), $file, $start_line, $end_line, $this->esc_maybe_null($namespace)]); |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* @inheritdocs |
112
|
|
|
*/ |
113
|
1 |
|
public function _method($name, $class, $file, $start_line, $end_line) { |
114
|
1 |
|
return $this->append_and_maybe_flush("methods", |
115
|
1 |
|
[null, $this->esc_str($name), $class, $file, $start_line, $end_line]); |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* @inheritdocs |
120
|
|
|
*/ |
121
|
1 |
|
public function _function($name, $file, $start_line, $end_line, $namespace = null) { |
122
|
1 |
|
return $this->append_and_maybe_flush("functions", |
123
|
1 |
|
[null, $this->esc_str($name), $file, $start_line, $end_line, $this->esc_maybe_null($namespace)]); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* @inheritdocs |
128
|
|
|
*/ |
129
|
1 |
|
public function _global($name) { |
130
|
1 |
|
return $this->append_and_maybe_flush("globals", |
131
|
1 |
|
[null, $this->esc_str($name)]); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* @inheritdocs |
136
|
|
|
*/ |
137
|
1 |
|
public function _language_construct($name) { |
138
|
1 |
|
return $this->append_and_maybe_flush("language_constructs", |
139
|
1 |
|
[null, $this->esc_str($name)]); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* @inheritdocs |
144
|
|
|
*/ |
145
|
1 |
|
public function _method_reference($name, $file, $line, $column) { |
146
|
1 |
|
return $this->append_and_maybe_flush("method_references", |
147
|
1 |
|
[null, $this->esc_str($name), $file, $line, $column]); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* @inheritdocs |
152
|
|
|
*/ |
153
|
1 |
|
public function _function_reference($name, $file, $line, $column) { |
154
|
1 |
|
return $this->append_and_maybe_flush("function_references", |
155
|
1 |
|
[null, $this->esc_str($name), $file, $line, $column]); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* @inheritdocs |
160
|
|
|
*/ |
161
|
1 |
|
public function _relation($left_entity, $relation, $right_entity, $file, $line) { |
162
|
1 |
|
$this->append_and_maybe_flush("relations", |
163
|
1 |
|
[$left_entity, $this->esc_str($relation), $right_entity, $file, $line]); |
164
|
1 |
|
} |
165
|
|
|
|
166
|
1 |
|
protected function insert_cache($table) { |
167
|
1 |
|
assert('array_key_exists($table, $this->tables)'); |
168
|
1 |
|
$fields = $this->tables[$table][1]; |
169
|
1 |
|
$which = &$this->caches[$table]; |
170
|
1 |
|
if (count($which) == 0) { |
171
|
1 |
|
return; |
172
|
|
|
} |
173
|
1 |
|
$stmt = "INSERT INTO $table (".implode(", ", $fields).") VALUES\n"; |
174
|
1 |
|
$values = []; |
175
|
1 |
|
foreach ($which as $v) { |
176
|
1 |
|
$values[] = "(".implode(", ", $v).")"; |
177
|
1 |
|
} |
178
|
1 |
|
$stmt .= implode(",\n", $values).";"; |
179
|
1 |
|
$this->connection->exec($stmt); |
180
|
1 |
|
$which = []; |
181
|
1 |
|
} |
182
|
|
|
|
183
|
1 |
|
protected function append_and_maybe_flush($table, $values) { |
184
|
1 |
|
if ($values[0] === null) { |
185
|
1 |
|
$id = $this->id_counter++; |
186
|
1 |
|
$values[0] = $id; |
187
|
1 |
|
} |
188
|
|
|
else { |
189
|
1 |
|
$id = null; |
190
|
|
|
} |
191
|
|
|
|
192
|
1 |
|
$which = &$this->caches[$table]; |
193
|
1 |
|
$which[] = $values; |
194
|
1 |
|
if (count($which) > $this->nodes_per_insert) { |
195
|
|
|
$this->insert_cache($table); |
196
|
|
|
} |
197
|
|
|
|
198
|
1 |
|
return $id; |
199
|
|
|
} |
200
|
1 |
|
protected function esc_str($str) { |
201
|
1 |
|
assert('is_string($str)'); |
202
|
1 |
|
return '"'.str_replace('"', '""', $str).'"'; |
203
|
|
|
} |
204
|
|
|
|
205
|
1 |
|
protected function esc_maybe_null($val) { |
206
|
1 |
|
assert('!is_string($val)'); |
207
|
1 |
|
if ($val === null) { |
208
|
1 |
|
return "NULL"; |
209
|
|
|
} |
210
|
1 |
|
return $val; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Write everything that currently is in cache to the database. |
215
|
|
|
* |
216
|
|
|
* @return null |
217
|
|
|
*/ |
218
|
1 |
|
public function write_cached_inserts() { |
219
|
1 |
|
foreach ($this->tables as $table => $_) { |
220
|
1 |
|
$this->insert_cache($table); |
221
|
1 |
|
} |
222
|
1 |
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Read the index from the database. |
226
|
|
|
* |
227
|
|
|
* @return Graph\IndexDB $index |
228
|
|
|
*/ |
229
|
1 |
|
public function to_graph_index() { |
230
|
1 |
|
$index = $this->build_graph_index_db(); |
231
|
1 |
|
$inserts = $this->get_inserts(); |
232
|
1 |
|
$this->write_inserts_to($index, $inserts); |
233
|
1 |
|
return $index; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
protected function build_graph_index_db() { |
237
|
|
|
return new Graph\IndexDB(); |
238
|
|
|
} |
239
|
|
|
|
240
|
1 |
|
protected function get_inserts() { |
241
|
1 |
|
$results = []; |
242
|
1 |
|
foreach ($this->tables as $key => $_) { |
243
|
1 |
|
if ($key == "relations") { |
244
|
1 |
|
continue; |
245
|
|
|
} |
246
|
1 |
|
$results[] = [$key, null, $this->select_all_from($key)]; |
247
|
1 |
|
} |
248
|
|
|
|
249
|
1 |
|
$count = count($results); |
250
|
1 |
|
$i = 0; |
251
|
1 |
|
$expected_id = 0; |
252
|
|
|
// TODO: This will loop forever if there are (unexpected) holes |
253
|
|
|
// in the sequence of ids. |
254
|
1 |
|
while ($count > 0) { |
255
|
1 |
|
if ($i >= $count) { |
256
|
1 |
|
$i = 0; |
257
|
1 |
|
} |
258
|
|
|
|
259
|
1 |
|
$current_res = $results[$i][1]; |
260
|
1 |
|
if ($current_res !== null) { |
261
|
1 |
|
if ($current_res["id"] != $expected_id) { |
262
|
1 |
|
$i++; |
263
|
1 |
|
continue; |
264
|
|
|
} |
265
|
|
|
|
266
|
1 |
|
$current_res["_which"] = $results[$i][0]; |
267
|
1 |
|
yield $current_res; |
268
|
1 |
|
$results[$i][1] = null; |
269
|
1 |
|
$expected_id++; |
270
|
1 |
|
} |
271
|
|
|
|
272
|
1 |
|
$next_res = $results[$i][2]->fetch(); |
273
|
1 |
|
if (!$next_res) { |
274
|
1 |
|
$count--; |
275
|
1 |
|
unset($results[$i]); |
276
|
1 |
|
$results = array_values($results); |
277
|
1 |
|
continue; |
278
|
|
|
} |
279
|
1 |
|
$results[$i][1] = $next_res; |
280
|
1 |
|
} |
281
|
|
|
|
282
|
1 |
|
$relations = $this->select_all_from("relations"); |
283
|
1 |
|
while($res = $relations->fetch()) { |
284
|
1 |
|
$res["_which"] = "relations"; |
285
|
1 |
|
yield $res; |
286
|
1 |
|
} |
287
|
1 |
|
} |
288
|
|
|
|
289
|
1 |
|
protected function select_all_from($table) { |
290
|
1 |
|
return $this->builder() |
291
|
1 |
|
->select($this->tables[$table][1]) |
292
|
1 |
|
->from($table) |
293
|
1 |
|
->execute(); |
294
|
|
|
} |
295
|
|
|
|
296
|
1 |
|
protected function write_inserts_to(Graph\IndexDB $index, \Iterator $inserts) { |
297
|
1 |
|
$id_map = []; |
298
|
1 |
|
foreach ($inserts as $insert) { |
299
|
1 |
|
if (isset($insert["file_id"])) { |
300
|
1 |
|
$insert["file_id"] = $id_map[(int)$insert["file_id"]]; |
301
|
1 |
|
} |
302
|
1 |
|
if (isset($insert["namespace_id"])) { |
303
|
1 |
|
$insert["namespace_id"] = $id_map[(int)$insert["namespace_id"]]; |
304
|
1 |
|
} |
305
|
1 |
|
if (isset($insert["class_id"])) { |
306
|
1 |
|
$insert["class_id"] = $id_map[(int)$insert["class_id"]]; |
307
|
1 |
|
} |
308
|
1 |
|
if (isset($insert["left_id"])) { |
309
|
1 |
|
$insert["left_id"] = $id_map[(int)$insert["left_id"]]; |
310
|
1 |
|
} |
311
|
1 |
|
if (isset($insert["right_id"])) { |
312
|
1 |
|
$insert["right_id"] = $id_map[(int)$insert["right_id"]]; |
313
|
1 |
|
} |
314
|
1 |
|
if (isset($insert["start_line"])) { |
315
|
1 |
|
$insert["start_line"] = (int)$insert["start_line"]; |
316
|
1 |
|
} |
317
|
1 |
|
if (isset($insert["end_line"])) { |
318
|
1 |
|
$insert["end_line"] = (int)$insert["end_line"]; |
319
|
1 |
|
} |
320
|
1 |
|
if (isset($insert["line"])) { |
321
|
1 |
|
$insert["line"] = (int)$insert["line"]; |
322
|
1 |
|
} |
323
|
1 |
|
if (isset($insert["column"])) { |
324
|
1 |
|
$insert["column"] = (int)$insert["column"]; |
325
|
1 |
|
} |
326
|
|
|
|
327
|
1 |
|
assert('array_key_exists($insert["_which"], $this->tables)'); |
328
|
1 |
|
$method = $this->tables[$insert["_which"]][0]; |
329
|
1 |
|
unset($insert["_which"]); |
330
|
1 |
|
if (isset($insert["id"])) { |
331
|
1 |
|
$id = $insert["id"]; |
332
|
1 |
|
unset($insert["id"]); |
333
|
1 |
|
$id_map[$id] = call_user_func_array([$index, $method], $insert); |
334
|
1 |
|
} |
335
|
|
|
else { |
336
|
1 |
|
call_user_func_array([$index, $method], $insert); |
337
|
|
|
} |
338
|
1 |
|
} |
339
|
1 |
|
} |
340
|
|
|
|
341
|
|
|
// INIT DATABASE |
342
|
|
|
|
343
|
1 |
|
public function init_table($name, Schema\Schema $schema, Schema\Table $file_table = null, Schema\Table $namespace_table = null) { |
344
|
1 |
|
assert('array_key_exists($name, $this->tables)'); |
345
|
1 |
|
$table = $schema->createTable($name); |
346
|
1 |
|
foreach ($this->tables[$name][1] as $field) { |
347
|
|
|
switch ($field) { |
348
|
1 |
|
case "id": |
349
|
1 |
|
case "file_id": |
350
|
1 |
|
case "class_id": |
351
|
1 |
|
case "left_id": |
352
|
1 |
|
case "right_id": |
353
|
1 |
|
case "start_line": |
354
|
1 |
|
case "end_line": |
355
|
1 |
|
case "line": |
356
|
1 |
View Code Duplication |
case "column": |
|
|
|
|
357
|
1 |
|
$table->addColumn |
358
|
1 |
|
($field, "integer" |
359
|
1 |
|
, ["notnull" => true, "unsigned" => true] |
360
|
1 |
|
); |
361
|
1 |
|
break; |
362
|
1 |
View Code Duplication |
case "namespace_id": |
|
|
|
|
363
|
1 |
|
$table->addColumn |
364
|
1 |
|
($field, "integer" |
365
|
1 |
|
, ["notnull" => false, "unsigned" => true] |
366
|
1 |
|
); |
367
|
1 |
|
break; |
368
|
1 |
|
case "path": |
369
|
1 |
|
case "source": |
370
|
1 |
|
case "name": |
371
|
1 |
|
case "relation": |
372
|
1 |
|
$table->addColumn |
373
|
1 |
|
($field, "string" |
374
|
1 |
|
, ["notnull" => true] |
375
|
1 |
|
); |
376
|
1 |
|
break; |
377
|
|
|
default: |
378
|
|
|
throw new \LogicException("Unknown field '$field'"); |
379
|
|
|
} |
380
|
1 |
|
if ($field == "id") { |
381
|
1 |
|
$table->setPrimaryKey(["id"]); |
382
|
1 |
|
} |
383
|
1 |
|
if ($field == "file_id") { |
384
|
1 |
|
$table->addForeignKeyConstraint |
385
|
1 |
|
( $file_table |
|
|
|
|
386
|
1 |
|
, array("file_id") |
387
|
1 |
|
, array("id") |
388
|
1 |
|
); |
389
|
1 |
|
} |
390
|
1 |
|
if ($field == "namespace_id") { |
391
|
1 |
|
$table->addForeignKeyConstraint |
392
|
1 |
|
( $namespace_table |
|
|
|
|
393
|
1 |
|
, array("namespace_id") |
394
|
1 |
|
, array("id") |
395
|
1 |
|
); |
396
|
1 |
|
} |
397
|
1 |
|
} |
398
|
1 |
|
return $table; |
399
|
|
|
} |
400
|
|
|
|
401
|
1 |
|
public function init_database_schema() { |
402
|
1 |
|
$schema = new Schema\Schema(); |
403
|
|
|
|
404
|
1 |
|
$file_table = $this->init_table("files", $schema); |
405
|
1 |
|
$namespace_table = $this->init_table("namespaces", $schema); |
406
|
1 |
|
$this->init_table("classes", $schema, $file_table, $namespace_table); |
407
|
1 |
|
$this->init_table("interfaces", $schema, $file_table, $namespace_table); |
408
|
1 |
|
$this->init_table("traits", $schema, $file_table, $namespace_table); |
409
|
1 |
|
$this->init_table("methods", $schema, $file_table); |
410
|
1 |
|
$this->init_table("functions", $schema, $file_table, $namespace_table); |
411
|
1 |
|
$this->init_table("globals", $schema); |
412
|
1 |
|
$this->init_table("language_constructs", $schema); |
413
|
1 |
|
$this->init_table("method_references", $schema, $file_table); |
414
|
1 |
|
$this->init_table("function_references", $schema, $file_table); |
415
|
1 |
|
$relation_table = $this->init_table("relations", $schema, $file_table); |
416
|
1 |
|
$relation_table->setPrimaryKey(["left_id", "relation", "right_id"]); |
417
|
|
|
|
418
|
1 |
|
$sync = new SingleDatabaseSynchronizer($this->connection); |
419
|
1 |
|
$sync->createSchema($schema); |
420
|
1 |
|
} |
421
|
|
|
} |
422
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.