import
[web.mtrack] / inc / database.php
1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
3
4 interface IMTrackDBExtension {
5   /** allows the extension an opportunity to adjust the environment;
6    * register sqlite functions or otherwise tweak parameters */
7   function onHandleCreated(PDO $db);
8 }
9
10 class MTrackDBSchema_Table {
11   var $name;
12   var $fields;
13   var $keys;
14   var $triggers;
15
16   /* compares two tables; returns true if they are identical,
17    * false if the definitions are altered */
18   function sameAs(MTrackDBSchema_Table $other) {
19     if ($this->name != $other->name) {
20       throw new Exception("can only compare tables with the same name!");
21     }
22     foreach (array('fields', 'keys', 'triggers') as $propname) {
23       if (!is_array($this->{$propname})) continue;
24       foreach ($this->{$propname} as $f) {
25         if (!isset($other->{$propname}[$f->name])) {
26 #          echo "$propname $f->name is new\n";
27           return false;
28         }
29         $o = clone $other->{$propname}[$f->name];
30         $f = clone $f;
31         unset($o->comment);
32         unset($f->comment);
33         if ($f != $o) {
34 #          echo "$propname $f->name are not equal\n";
35 #          var_dump($f);
36 #          var_dump($o);
37           return false;
38         }
39       }
40       if (!is_array($other->{$propname})) continue;
41       foreach ($other->{$propname} as $f) {
42         if (!isset($this->{$propname}[$f->name])) {
43 #          echo "$propname $f->name was deleted\n";
44           return false;
45         }
46       }
47     }
48
49     return true;
50   }
51 }
52
53 interface IMTrackDBSchema_Driver {
54   function setDB(PDO $db);
55   function determineVersion();
56   function createTable(MTrackDBSchema_Table $table);
57   function alterTable(MTrackDBSchema_Table $from, MTrackDBSchema_Table $to);
58   function dropTable(MTrackDBSchema_Table $table);
59 };
60
61 class MTrackDBSchema_Generic implements IMTrackDBSchema_Driver {
62   var $db;
63   var $typemap = array();
64
65   function setDB(PDO $db) {
66     $this->db = $db;
67   }
68
69   function determineVersion() {
70     try {
71       $q = $this->db->query('select version from mtrack_schema');
72       if ($q) {
73         foreach ($q as $row) {
74           return $row[0];
75         }
76       }
77     } catch (Exception $e) {
78     }
79     return null;
80   }
81
82   function computeFieldCreate($f) {
83     $str = "\t$f->name ";
84     $str .= isset($this->typemap[$f->type]) ? $this->typemap[$f->type] : $f->type;
85     if (isset($f->nullable) && $f->nullable == '0') {
86       $str .= ' NOT NULL ';
87     }
88     if (isset($f->default)) {
89       if (!strlen($f->default)) {
90         $str .= " DEFAULT ''";
91       } else {
92         $str .= " DEFAULT $f->default";
93       }
94     }
95     return $str;
96   }
97
98   function computeIndexCreate($table, $k) {
99     switch ($k->type) {
100       case 'unique':
101         $kt = ' UNIQUE ';
102         break;
103       case 'multiple':
104       default:
105         $kt = '';
106     }
107     return "CREATE $kt INDEX $k->name on $table->name (" . join(', ', $k->fields) . ")";
108   }
109
110   function createTable(MTrackDBSchema_Table $table)
111   {
112     echo "Create $table->name\n";
113
114     $pri_key = null;
115
116     $sql = array();
117     foreach ($table->fields as $f) {
118       if ($f->type == 'autoinc') {
119         $pri_key = $f->name;
120       }
121       $str = $this->computeFieldCreate($f);
122       $sql[] = $str;
123     }
124
125     if (is_array($table->keys)) foreach ($table->keys as $k) {
126       if ($k->type != 'primary') continue;
127       if ($pri_key !== null) continue;
128       $sql[] = "\tprimary key (" . join(', ', $k->fields) . ")";
129     }
130
131     $sql = "CREATE TABLE $table->name (\n" .
132       join(",\n", $sql) .
133       ")\n";
134
135 #    echo $sql;
136
137     $this->db->exec($sql);
138
139     if (is_array($table->keys)) foreach ($table->keys as $k) {
140       if ($k->type == 'primary') continue;
141       $this->db->exec($this->computeIndexCreate($table, $k));
142     }
143   }
144
145   function alterTable(MTrackDBSchema_Table $from, MTrackDBSchema_Table $to)
146   {
147     /* if keys have changed, we drop the old key definitions before changing the columns */
148
149     echo "Need to alter $from->name\n";
150     throw new Exception("bang!");
151   }
152
153   function dropTable(MTrackDBSchema_Table $table)
154   {
155     echo "Drop $table->name\n";
156     $this->db->exec("drop table $table->name");
157   }
158 }
159
160 class MTrackDBSchema_SQLite extends MTrackDBSchema_Generic {
161
162   function determineVersion() {
163     /* older versions did not have a schema version table, so we dance
164      * around a little bit, but only for sqlite, as those older versions
165      * didn't support other databases */
166     try {
167       $q = $this->db->query('select version from mtrack_schema');
168       if ($q) {
169         foreach ($q as $row) {
170           return $row[0];
171         }
172       }
173     } catch (Exception $e) {
174     }
175
176     /* do we have any tables at all? if we do, we treat that as schema
177      * version 0 */
178     foreach ($this->db->query('select count(*) from sqlite_master') as $row) {
179       if ($row[0] > 0) {
180         $this->db->exec(
181           'create table mtrack_schema (version integer not null)');
182         return 0;
183       }
184     }
185     return null;
186   }
187
188   var $typemap = array(
189     'autoinc' => 'INTEGER PRIMARY KEY AUTOINCREMENT',
190   );
191
192   function createTable(MTrackDBSchema_Table $table)
193   {
194     parent::createTable($table);
195   }
196
197   function alterTable(MTrackDBSchema_Table $from, MTrackDBSchema_Table $to)
198   {
199     $tname = $from->name . '_' . uniqid();
200
201     $sql = array();
202     foreach ($to->fields as $f) {
203       if ($f->type == 'autoinc') {
204         $pri_key = $f->name;
205       }
206       $str = $this->computeFieldCreate($f);
207       $sql[] = $str;
208     }
209
210     $sql = "CREATE TEMPORARY TABLE $tname (\n" .
211       join(",\n", $sql) .
212       ")\n";
213
214     $this->db->exec($sql);
215
216     /* copy old data into this table */
217     $sql = "INSERT INTO $tname (";
218     $names = array();
219     foreach ($from->fields as $f) {
220       if (!isset($to->fields[$f->name])) continue;
221       $names[] = $f->name;
222     }
223     $sql .= join(', ', $names);
224     $sql .= ") SELECT " . join(', ', $names) . " from $from->name";
225
226     #echo "$sql\n";
227     $this->db->exec($sql);
228
229     $this->db->exec("DROP TABLE $from->name");
230     $this->createTable($to);
231     $sql = "INSERT INTO $from->name (";
232     $names = array();
233     foreach ($from->fields as $f) {
234       if (!isset($to->fields[$f->name])) continue;
235       $names[] = $f->name;
236     }
237     $sql .= join(', ', $names);
238     $sql .= ") SELECT " . join(', ', $names) . " from $tname";
239     #echo "$sql\n";
240     $this->db->exec($sql);
241     $this->db->exec("DROP TABLE $tname");
242   }
243
244
245 }
246
247 class MTrackDBSchema_pgsql extends MTrackDBSchema_Generic {
248   var $typemap = array(
249     'autoinc' => 'SERIAL UNIQUE',
250     'timestamp' => 'timestamp with time zone',
251     'blob' => 'bytea',
252   );
253
254   function alterTable(MTrackDBSchema_Table $from, MTrackDBSchema_Table $to)
255   {
256     $sql = array();
257     $actions = array();
258
259     /* if keys have changed, we drop the old key definitions before changing the columns */
260     if (is_array($from->keys)) foreach ($from->keys as $k) {
261       if (!isset($to->keys[$k->name]) || $to->keys[$k->name] != $k) {
262         if ($k->type == 'primary') {
263           $actions[] = "DROP CONSTRAINT {$from->name}_pkey";
264         } else {
265           $sql[] = "DROP INDEX $k->name";
266         }
267       }
268     }
269
270     foreach ($from->fields as $f) {
271       if (!isset($to->fields[$f->name])) {
272         $actions[] = "DROP COLUMN $f->name";
273         continue;
274       }
275     }
276     foreach ($to->fields as $f) {
277       if (isset($from->fields[$f->name])) continue;
278       $actions[] = "ADD COLUMN " . $this->computeFieldCreate($f);
279     }
280
281     /* changed and new keys */
282     if (is_array($from->keys)) foreach ($from->keys as $k) {
283       if (isset($to->keys[$k->name]) && $to->keys[$k->name] != $k) {
284         if ($k->type == 'primary') {
285           $actions[] = "ADD primary key (" . join(', ', $k->fields) . ")";
286         } else {
287           $sql[] = $this->computeIndexCreate($to, $k);
288         }
289       }
290     }
291     if (is_array($to->keys)) foreach ($to->keys as $k) {
292       if (isset($from->keys[$k->name])) continue;
293       if ($k->type == 'primary') {
294         $actions[] = "ADD primary key (" . join(', ', $k->fields) . ")";
295       } else {
296         $sql[] = $this->computeIndexCreate($to, $k);
297       }
298     }
299
300     if (count($actions)) {
301       $sql[] = "ALTER TABLE $from->name " . join(",\n", $actions);
302     }
303     echo "Need to alter $from->name\n";
304     echo "SQL:\n";
305     var_dump($sql);
306     foreach ($sql as $s) {
307       $this->db->exec($s);
308     }
309   }
310 }
311
312 class MTrackDBSchema {
313   var $tables;
314   var $version;
315   var $post;
316
317   function __construct($filename) {
318     $s = simplexml_load_file($filename);
319
320     $this->version = (int)$s['version'];
321
322     /* fabricate a table to hold the schema info */
323     $table = new MTrackDBSchema_Table;
324     $table->name = 'mtrack_schema';
325     $f = new stdclass;
326     $f->name = 'version';
327     $f->type = 'integer';
328     $f->nullable = '0';
329     $table->fields[$f->name] = $f;
330     $this->tables[$table->name] = $table;
331
332     foreach ($s->table as $t) {
333       $table = new MTrackDBSchema_Table;
334       $table->name = (string)$t['name'];
335
336       foreach ($t->field as $f) {
337         $F = new stdclass;
338         foreach ($f->attributes() as $k => $v) {
339           $F->{(string)$k} = (string)$v;
340         }
341         if (isset($f->comment)) {
342           $F->comment = (string)$f->comment;
343         }
344         $table->fields[$F->name] = $F;
345       }
346       foreach ($t->key as $k) {
347         $K = new stdclass;
348         $K->fields = array();
349         if (isset($k['type'])) {
350           $K->type = (string)$k['type'];
351         } else {
352           $K->type = 'primary';
353         }
354         foreach ($k->field as $f) {
355           $K->fields[] = (string)$f;
356         }
357         if (isset($k['name'])) {
358           $K->name = (string)$k['name'];
359         } else {
360           $K->name = sprintf("idx_%s_%s", $table->name, join('_', $K->fields));
361         }
362         $table->keys[$K->name] = $K;
363       }
364
365       $this->tables[$table->name] = $table;
366     }
367     foreach ($s->post as $p) {
368       $this->post[(string)$p['driver']] = (string)$p;
369     }
370
371     /* apply custom ticket fields */
372     if (isset($this->tables['tickets'])) {
373       $table = $this->tables['tickets'];
374       $custom = MTrackTicket_CustomFields::getInstance();
375       foreach ($custom->fields as $field) {
376         $f = new stdclass;
377         $f->name = $field->name;
378         $f->type = 'text';
379         $table->fields[$f->name] = $f;
380       }
381     }
382   }
383 }
384
385 class MTrackDB {
386   static $db = null;
387   static $extensions = array();
388   static $queries = 0;
389   static $query_strings = array();
390
391   static function registerExtension(IMTrackDBExtension $ext) {
392     self::$extensions[] = $ext;
393   }
394
395   // given a unix timestamp, return a value timestamp string
396   // suitable for use with the database
397   static function unixtime($unix) {
398     list($unix) = explode('.', $unix, 2);
399     if ($unix == 0) {
400       return null;
401     }
402     if ($unix < 10) {
403       throw new Exception("unix time $unix is too small\n");
404     }
405     $d = date_create("@$unix", new DateTimeZone('UTC'));
406     // 2008-12-22T05:42:42.285445Z
407     if (!is_object($d)) {
408       throw new Exception("failed to create date for time $unix");
409     }
410     return $d->format('Y-m-d\TH:i:s.u\Z');
411   }
412
413   static function get() {
414     if (self::$db == null) {
415       $dsn = MTrackConfig::get('core', 'dsn');
416       if ($dsn === null) {
417         $dsn = 'sqlite:' . MTrackConfig::get('core', 'dblocation');
418       }
419       $db = new PDO($dsn);
420       $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
421       self::$db = $db;
422
423       if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'sqlite') {
424         $db->sqliteCreateAggregate('mtrack_group_concat',
425             array('MTrackDB', 'group_concat_step'),
426             array('MTrackDB', 'group_concat_final'));
427
428         $db->sqliteCreateFunction('mtrack_cleanup_attachments',
429             array('MTrackAttachment', 'attachment_row_deleted'));
430       }
431
432       foreach (self::$extensions as $ext) {
433         $ext->onHandleCreated($db);
434       }
435     }
436     return self::$db;
437   }
438
439   static function lastInsertId($tablename, $keyfield) {
440     if (!strlen($tablename) || !strlen($keyfield)) {
441       throw new Exception("missing tablename or keyfield");
442     }
443     if (self::$db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') {
444       return self::$db->lastInsertId($tablename . '_' . $keyfield . '_seq');
445     } else {
446       return self::$db->lastInsertId();
447     }
448   }
449
450   static function group_concat_step($context, $rownum, $value)
451   {
452     if (!is_array($context)) {
453       $context = array();
454     }
455     $context[] = $value;
456     return $context;
457   }
458
459   static function group_concat_final($context, $rownum)
460   {
461     if ($context === null) {
462       return null;
463     }
464     asort($context);
465     return join(", ", $context);
466   }
467
468   static function esc($str) {
469     return "'" . str_replace("'", "''", $str) . "'";
470   }
471
472   /* issue a query, passing optional parameters */
473   static function q($sql) {
474     self::$queries++;
475     if (isset(self::$query_strings[$sql])) {
476       self::$query_strings[$sql]++;
477     } else {
478       self::$query_strings[$sql] = 1;
479     }
480     $params = func_get_args();
481     array_shift($params);
482     $db = self::get();
483 #      echo "<br>SQL: $sql\n";
484 #      var_dump($params);
485 #echo "<br>";
486     try {
487       if (count($params)) {
488         $q = $db->prepare($sql);
489         $q->execute($params);
490       } else {
491         $q = $db->query($sql);
492       }
493     } catch (Exception $e) {
494       echo $e->getMessage();
495       throw $e;
496     }
497     return $q;
498   }
499 }
500