1<?php
2/**
3 * DokuWiki Plugin strata (Helper Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Brend Wanders <b.wanders@utwente.nl>
7 */
8
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) die('Meh.');
11
12/**
13 * The triples helper is responsible for querying.
14 */
15class helper_plugin_strata_triples extends DokuWiki_Plugin {
16    public static $readable = 'data';
17    public static $writable = 'data';
18
19    function __construct() {
20        $this->_initialize();
21    }
22
23    function getMethods() {
24        $result = array();
25        return $result;
26    }
27
28    /**
29     * Expands tokens in the DSN.
30     *
31     * @param str string the string to process
32     * @return a string with replaced tokens
33     */
34    function _expandTokens($str) {
35        global $conf;
36        $tokens    = array('@METADIR@');
37        $replacers = array($conf['metadir']);
38        return str_replace($tokens,$replacers,$str);
39    }
40
41    /**
42     * Initializes the triple helper.
43     *
44     * @param dsn string an optional alternative DSN
45     * @return true if initialization succeeded, false otherwise
46     */
47    function _initialize() {
48        // load default DSN
49        $dsn = $this->getConf('default_dsn');
50        $dsn = $this->_expandTokens($dsn);
51
52        $this->_dsn = $dsn;
53
54        // construct driver
55        list($driver,$connection) = explode(':',$dsn,2);
56        $driverFile = DOKU_PLUGIN."strata/driver/$driver.php";
57        if(!@file_exists($driverFile)) {
58            msg(sprintf($this->getLang('error_triples_nodriver'), $driver), -1);
59            return false;
60        }
61        require_once($driverFile);
62        $driverClass = "plugin_strata_driver_$driver";
63        $this->_db = new $driverClass($this->getConf('debug'));
64
65        // connect driver
66        if(!$this->_db->connect($dsn)) {
67            return false;
68        }
69
70        // initialize database if necessary
71        if(!$this->_db->isInitialized()) {
72            $this->_db->initializeDatabase();
73        }
74
75
76        return true;
77    }
78
79    /**
80     * Makes the an SQL expression case insensitive.
81     *
82     * @param a string the expression to process
83     * @return a SQL expression
84     */
85    function _ci($a) {
86        return $this->_db->ci($a);
87    }
88
89    /**
90     * Constructs a case insensitive string comparison in SQL.
91     *
92     * @param a string the left-hand side
93     * @param b string the right-hand side
94     *
95     * @return a case insensitive SQL string comparison
96     */
97    function _cic($a, $b) {
98        return $this->_ci($a).' = '.$this->_ci($b);
99    }
100
101    /**
102     * Begins a preview.
103     */
104    function beginPreview() {
105        $this->_db->beginTransaction();
106    }
107
108    /**
109     * Ends a preview.
110     */
111    function endPreview() {
112        $this->_db->rollback();
113    }
114
115    /**
116     * Removes all triples matching the given triple pattern. One or more parameters
117     * can be left out to indicate 'any'.
118     */
119    function removeTriples($subject=null, $predicate=null, $object=null, $graph=null) {
120        // construct triple filter
121        $filters = array('1 = 1');
122        foreach(array('subject','predicate','object','graph') as $param) {
123            if($$param != null) {
124                $filters[]=$this->_cic($param, '?');
125                $values[] = $$param;
126            }
127        }
128
129        $sql = "DELETE FROM ".self::$writable." WHERE ". implode(" AND ", $filters);
130
131        // prepare query
132        $query = $this->_db->prepare($sql);
133        if($query == false) return;
134
135        // execute query
136        $res = $query->execute($values);
137        if($res === false) {
138            $error = $query->errorInfo();
139            msg(sprintf($this->getLang('error_triples_remove'),hsc($error[2])),-1);
140        }
141
142        $query->closeCursor();
143    }
144
145    /**
146     * Fetches all triples matching the given triple pattern. Onr or more of
147     * parameters can be left out to indicate 'any'.
148     */
149    function fetchTriples($subject=null, $predicate=null, $object=null, $graph=null) {
150	global $ID;
151        // construct filter
152
153        $filters = array('1 = 1');
154        foreach(array('subject','predicate','object','graph') as $param) {
155            if($$param != null) {
156                $filters[]=$this->_cic($param,'?');
157                $values[] = $$param;
158            }
159        }
160
161	$scopeRestriction = ($this->getConf('scoped')? ' AND graph like "'.getNS($ID).'%"':"" );
162/*
163        if ($this->getConf('scoped'))
164	{
165		$NS = getNS($ID);
166		if ($NS=="")
167			{ $scopeRestriction = "and graph not like '%:%'"; }
168
169		else
170			{ $scopeRestriction = "and graph like".$NS.":%"; }
171
172	}
173	else { $scopeRestriction="" };
174*/
175
176        $sql = "SELECT subject, predicate, object, graph FROM ".self::$readable." WHERE ". implode(" AND ", $filters).$scopeRestriction;
177
178        // prepare queyr
179        $query = $this->_db->prepare($sql);
180        if($query == false) return;
181
182        // execute query
183        $res = $query->execute($values);
184
185        if($res === false) {
186            $error = $query->errorInfo();
187            msg(sprintf($this->getLang('error_triples_fetch'),hsc($error[2])),-1);
188        }
189
190        // fetch results and return them
191        $result = $query->fetchAll(PDO::FETCH_ASSOC);
192        $query->closeCursor();
193
194        return $result;
195    }
196
197    /**
198     * Adds a single triple.
199     * @param subject string
200     * @param predicate string
201     * @param object string
202     * @param graph string
203     * @return true of triple was added succesfully, false if not
204     */
205    function addTriple($subject, $predicate, $object, $graph) {
206        return $this->addTriples(array(array('subject'=>$subject, 'predicate'=>$predicate, 'object'=>$object)), $graph);
207    }
208
209    /**
210     * Adds multiple triples.
211     * @param triples array contains all triples as arrays with subject, predicate and object keys
212     * @param graph string graph name
213     * @return true if the triples were comitted, false otherwise
214     */
215    function addTriples($triples, $graph) {
216        // prepare insertion query
217        $sql = "INSERT INTO ".self::$writable."(subject, predicate, object, graph) VALUES(?, ?, ?, ?)";
218        $query = $this->_db->prepare($sql);
219        if($query == false) return false;
220
221        // put the batch in a transaction
222        $this->_db->beginTransaction();
223        foreach($triples as $t) {
224            // insert a single triple
225            $values = array($t['subject'],$t['predicate'],$t['object'],$graph);
226            $res = $query->execute($values);
227
228            // handle errors
229            if($res === false) {
230                $error = $query->errorInfo();
231                msg(sprintf($this->getLang('error_triples_add'),hsc($error[2])),-1);
232                $this->_db->rollBack();
233                return false;
234            }
235            $query->closeCursor();
236        }
237
238        // commit and return
239        return $this->_db->commit();
240    }
241
242    /**
243     * Executes the given abstract query tree as a query on the store.
244     *
245     * @param query array an abstract query tree
246     * @return an iterator over the resulting rows
247     */
248    function queryRelations($queryTree) {
249        // create the SQL generator, and generate the SQL query
250        $generator = new strata_sql_generator($this);
251        list($sql, $literals, $projected, $grouped) = $generator->translate($queryTree);
252
253        // prepare the query
254        $query = $this->_db->prepare($sql);
255        if($query === false) {
256            return false;
257        }
258
259        // execute the query
260        $res = $query->execute($literals);
261        if($res === false) {
262            $error = $query->errorInfo();
263            msg(sprintf($this->getLang('error_triples_query'),hsc($error[2])),-1);
264            if($this->getConf('debug')) {
265                msg(sprintf($this->getLang('debug_sql'),hsc($sql)),-1);
266                msg(sprintf($this->getLang('debug_literals'), hsc(print_r($literals,1))),-1);
267            }
268            return false;
269        }
270
271        // wrap the results in an iterator, and return it
272        if($queryTree['grouping'] === false) {
273            return new strata_relations_iterator($query, $projected);
274        } else {
275            return new strata_aggregating_iterator($query, $projected, $grouped);
276        }
277    }
278
279    /**
280     * Executes the abstract query tree, and returns all properties of the matching subjects.
281     * This method assumes that the root is a 'select' node.
282     *
283     * @param query array the abstract query tree
284     * @return an iterator over the resources
285     */
286    function queryResources($query) {
287        // We transform the given query into a resource-centric query as follows:
288        //   Remember the single projected variable Vx.
289        //   Append two variables ?__p and ?__o to the projection
290        //   Add an extra triple pattern (Vx, ?__p, ?__o)
291        //   Append Vx to the ordering
292        // The query is ready for execution. Result set can be transformed into a
293        // resource-centric view by fetching all triples related to a single subject
294        // (each subject is in a single continuous block, due to the ordering)
295
296        // add extra tuple
297        $query['group'] = array(
298            'type'=>'and',
299            'lhs'=>$query['group'],
300            'rhs'=>array(
301                'type'=>'triple',
302                'subject'=>array('type'=>'variable','text'=>$query['projection'][0]),
303                'predicate'=>array('type'=>'variable','text'=>'__predicate'),
304                'object'=>array('type'=>'variable','text'=>'__object')
305            )
306        );
307
308        // fix projection list
309        $query['projection'] = array(
310            $query['projection'][0],
311            '__predicate',
312            '__object'
313        );
314
315        // append tuple ordering
316        $query['ordering'][] = array(
317            'variable'=>$query['projection'][0],
318            'direction'=>'asc'
319        );
320
321        // remove grouping
322        $query['grouping'] = false;
323
324        // execute query
325        $result = $this->queryRelations($query);
326
327        if($result === false) {
328            return false;
329        }
330
331        // invoke iterator that's going to aggregate the resulting relations
332        return new strata_resource_iterator($result,$query['projection']);
333    }
334}
335
336/**
337 * SQL generator.
338 */
339class strata_sql_generator {
340    /**
341     * Stores all literal values keyed to their placeholder.
342     */
343    private $literals = array();
344
345    /**
346     * Stores all projected variables.
347     */
348    private $projected = array();
349
350    /**
351     * Stores all grouped variables.
352     */
353    private $grouped = array();
354
355    /**
356     * Constructor.
357     */
358    function __construct($triples) {
359        $this->_triples = $triples;
360        $this->_db = $this->_triples->_db;
361    }
362
363    /**
364     * Passes through localisation calls.
365     */
366    function getLang($key) {
367        return $this->_triples->getLang($key);
368    }
369
370    /**
371     * Wrap SQL expression in case insensitivisation.
372     */
373    function _ci($a) {
374        return $this->_triples->_ci($a);
375    }
376
377    /**
378     * Alias generator.
379     */
380    private $_aliasCounter = 0;
381    function _alias($prefix='a') {
382        return $prefix.($this->_aliasCounter++);
383    }
384
385    /**
386     * All used literals.
387     */
388    private $_literalLookup = array();
389    private $_variableLookup = array();
390
391    /**
392     * Name generator. Makes the distinction between literals
393     * and variables, as they can have the same spelling (and
394     * frequently do in simple queries).
395     */
396    function _name($term) {
397        if($term['type'] == 'variable') {
398            // always try the cache
399            if(empty($this->_variableLookup[$term['text']])) {
400                $this->_variableLookup[$term['text']] = $this->_alias('v');
401            }
402
403            return $this->_variableLookup[$term['text']];
404        } elseif($term['type'] == 'literal') {
405            // always try the cache
406            if(empty($this->_literalLookup[$term['text']])) {
407                // use aliases to represent literals
408                $this->_literalLookup[$term['text']] = $this->_alias('lit');
409            }
410
411            // return literal name
412            return $this->_literalLookup[$term['text']];
413        }
414    }
415
416    /**
417     * Test whether two things are equal (i.e., equal variables, or equal literals)
418     */
419    function _patternEquals($pa, $pb) {
420        return $pa['type'] == $pb['type'] && $pa['text'] == $pb['text'];
421    }
422
423    /**
424     * Generates a conditional for the given triple pattern.
425     */
426    function _genCond($tp) {
427        $conditions = array();
428
429        // the subject is a literal
430        if($tp['subject']['type'] == 'literal') {
431            $id = $this->_alias('qv');
432            $conditions[] = $this->_ci('subject').' = '.$this->_ci(':'.$id);
433            $this->literals[$id] = $tp['subject']['text'];
434        }
435
436        // the predicate is a literal
437        if($tp['predicate']['type'] == 'literal') {
438            $id = $this->_alias('qv');
439            $conditions[] = $this->_ci('predicate').' = '.$this->_ci(':'.$id);
440            $this->literals[$id] = $tp['predicate']['text'];
441        }
442
443        // the object is a literal
444        if($tp['object']['type'] == 'literal') {
445            $id = $this->_alias('qv');
446            $conditions[] = $this->_ci('object').' = '.$this->_ci(':'.$id);
447            $this->literals[$id] = $tp['object']['text'];
448        }
449
450        // subject equals predicate
451        if($this->_patternEquals($tp['subject'],$tp['predicate'])) {
452            $conditions[] = $this->_ci('subject').' = '.$this->_ci('predicate');
453        }
454
455        // subject equals object
456        if($this->_patternEquals($tp['subject'],$tp['object'])) {
457            $conditions[] = $this->_ci('subject').' = '.$this->_ci('object');
458        }
459
460        // predicate equals object
461        if($this->_patternEquals($tp['predicate'],$tp['object'])) {
462            $conditions[] = $this->_ci('predicate').' = '.$this->_ci('object');
463        }
464
465        if(count($conditions)!=0) {
466            return implode(' AND ',$conditions);
467        } else {
468            return '1 = 1';
469        }
470    }
471
472    /**
473     * Generates a projection for the given triple pattern.
474     */
475    function _genPR($tp) {
476        $list = array();
477
478        // always project the subject
479        $list[] = 'subject AS '.$this->_name($tp['subject']);
480
481        // project the predicate if it's different from the subject
482        if(!$this->_patternEquals($tp['subject'], $tp['predicate'])) {
483            $list[] = 'predicate AS '.$this->_name($tp['predicate']);
484        }
485
486        // project the object if it's different from the subject and different from the predicate
487        if(!$this->_patternEquals($tp['subject'], $tp['object']) && !$this->_patternEquals($tp['predicate'],$tp['object'])) {
488            $list[] = 'object AS '.$this->_name($tp['object']);
489        }
490
491        return implode(', ',$list);
492    }
493
494    /**
495     * Translates a triple pattern into a graph pattern.
496     */
497    function _trans_tp($tp) {
498	global $ID;
499        $terms = array();
500
501        // the subject is a variable
502        if($tp['subject']['type'] == 'variable') {
503            $terms[] = $this->_name($tp['subject']);
504        }
505
506        // the predicate is a variable
507        if($tp['predicate']['type'] == 'variable') {
508            $terms[] = $this->_name($tp['predicate']);
509        }
510
511        // the object is a variable
512        if($tp['object']['type'] == 'variable') {
513            $terms[] = $this->_name($tp['object']);
514        }
515
516        $scopeRestriction = ($this->_triples->getConf('scoped')? ' AND graph like "'.getNS($ID).'%"':"" );
517        return array(
518            'sql'=>'SELECT '.$this->_genPR($tp).' FROM '.helper_plugin_strata_triples::$readable.' WHERE '.$this->_genCond($tp).$scopeRestriction,
519            'terms'=>array_unique($terms)
520        );
521    }
522
523    /**
524     * Translates a group operation on the two graph patterns.
525     */
526    function _trans_group($gp1, $gp2, $join) {
527        // determine the resulting terms
528        $terms = array_unique(array_merge($gp1['terms'], $gp2['terms']));
529
530        // determine the overlapping terms (we need to coalesce these)
531        $common = array_intersect($gp1['terms'], $gp2['terms']);
532
533        // determine the non-overlapping terms (we can project them directly)
534        $fields = array_diff($terms, $common);
535
536        // handle overlapping terms by coalescing them into a single term
537        if(count($common)>0) {
538            $intersect = array();
539            foreach($common as $c) {
540                $intersect[] = '('.$this->_ci('r1.'.$c).' = '.$this->_ci('r2.'.$c).' OR r1.'.$c.' IS NULL OR r2.'.$c.' IS NULL)';
541                $fields[]='COALESCE(r1.'.$c.', r2.'.$c.') AS '.$c;
542            }
543            $intersect = implode(' AND ',$intersect);
544        } else {
545            $intersect = '(1=1)';
546        }
547
548        $fields = implode(', ',$fields);
549
550        return array(
551            'sql'=>'SELECT DISTINCT '.$fields.' FROM ('.$gp1['sql'].') AS r1 '.$join.' ('.$gp2['sql'].') AS r2 ON '.$intersect,
552            'terms'=>$terms
553        );
554    }
555
556    /**
557     * Translate an optional operation.
558     */
559    function _trans_opt($query) {
560        $gp1 = $this->_dispatch($query['lhs']);
561        $gp2 = $this->_dispatch($query['rhs']);
562        return $this->_trans_group($gp1, $gp2, 'LEFT OUTER JOIN');
563    }
564
565    /**
566     * Translate an and operation.
567     */
568    function _trans_and($query) {
569        $gp1 = $this->_dispatch($query['lhs']);
570        $gp2 = $this->_dispatch($query['rhs']);
571        return $this->_trans_group($gp1, $gp2, 'INNER JOIN');
572    }
573
574    /**
575     * Translate a filter operation. The filters are a conjunction of separate expressions.
576     */
577    function _trans_filter($query) {
578        $gp = $this->_dispatch($query['lhs']);
579        $fs = $query['rhs'];
580
581        $filters = array();
582
583        foreach($fs as $f) {
584            // determine representation of left-hand side
585            if($f['lhs']['type'] == 'variable') {
586                $lhs = $this->_name($f['lhs']);
587            } else {
588                $id = $this->_alias('qv');
589                $lhs = ':'.$id;
590                $this->literals[$id] = $f['lhs']['text'];
591            }
592
593            // determine representation of right-hand side
594            if($f['rhs']['type'] == 'variable') {
595                $rhs = $this->_name($f['rhs']);
596            } else {
597                $id = $this->_alias('qv');
598                $rhs = ':'.$id;
599                $this->literals[$id] = $f['rhs']['text'];
600            }
601
602            // the escaping constants (head, tail and modifier)
603            $eh= "REPLACE(REPLACE(REPLACE(";
604            $et= ",'!','!!'),'_','!_'),'%','!%')";
605            $em= " ESCAPE '!'";
606
607            // handle different operators
608            switch($f['operator']) {
609                case '=':
610                case '!=':
611                    $filters[] = '( ' . $this->_ci($lhs) . ' '.$f['operator'].' ' . $this->_ci($rhs). ' )';
612                    break;
613                case '>':
614                case '<':
615                case '>=':
616                case '<=':
617                    $filters[] = '( ' . $this->_triples->_db->castToNumber($lhs) . ' ' . $f['operator'] . ' ' . $this->_triples->_db->castToNumber($rhs) . ' )';
618                    break;
619                case '~':
620                    $filters[] = '( ' . $this->_ci($lhs) . ' '.$this->_db->stringCompare().' '. $this->_ci('(\'%\' || ' .$eh.$rhs.$et. ' || \'%\')') .$em. ')';
621                    break;
622                case '!~':
623                    $filters[] = '( ' . $this->_ci($lhs) . ' NOT '.$this->_db->stringCompare().' '. $this->_ci('(\'%\' || ' . $eh.$rhs.$et. ' || \'%\')') .$em. ')';
624                    break;
625                case '^~':
626                    $filters[] = '( ' . $this->_ci($lhs) . ' '.$this->_db->stringCompare().' ' .$this->_ci('('. $eh.$rhs.$et . ' || \'%\')').$em. ')';
627                    break;
628                case '!^~':
629                    $filters[] = '( ' . $this->_ci($lhs) . ' NOT '.$this->_db->stringCompare().' ' .$this->_ci('('. $eh.$rhs.$et . ' || \'%\')').$em. ')';
630                    break;
631                case '$~':
632                    $filters[] = '( ' . $this->_ci($lhs) . ' '.$this->_db->stringCompare().' '.$this->_ci('(\'%\' || ' . $eh.$rhs.$et. ')') .$em. ')';
633                    break;
634                case '!$~':
635                    $filters[] = '( ' . $this->_ci($lhs) . ' NOT '.$this->_db->stringCompare().' '.$this->_ci('(\'%\' || ' . $eh.$rhs.$et. ')') .$em. ')';
636                    break;
637
638                default:
639            }
640        }
641
642        $filters = implode(' AND ', $filters);
643
644        return array(
645            'sql'=>'SELECT * FROM ('.$gp['sql'].') r WHERE '.$filters,
646            'terms'=>$gp['terms']
647        );
648    }
649
650    /**
651     * Translate minus operation.
652     */
653    function _trans_minus($query) {
654        $gp1 = $this->_dispatch($query['lhs']);
655        $gp2 = $this->_dispatch($query['rhs']);
656
657        // determine overlapping terms (we need to substitute these)
658        $common = array_intersect($gp1['terms'], $gp2['terms']);
659
660        // create conditional that 'substitutes' terms by requiring equality
661        $terms = array();
662        foreach($common as $c) {
663            $terms[] = '('.$this->_ci('r1.'.$c).' = '.$this->_ci('r2.'.$c).')';
664        }
665
666        if(count($terms)>0) {
667            $terms = implode(' AND ',$terms);
668        } else {
669            $terms = '1=1';
670        }
671
672        return array(
673            'sql'=>'SELECT DISTINCT * FROM ('.$gp1['sql'].') r1 WHERE NOT EXISTS (SELECT * FROM ('.$gp2['sql'].') r2 WHERE '.$terms.')',
674            'terms'=>$gp1['terms']
675        );
676    }
677
678    /**
679     * Translate union operation.
680     */
681    function _trans_union($query) {
682        // dispatch the child graph patterns
683        $gp1 = $this->_dispatch($query['lhs']);
684        $gp2 = $this->_dispatch($query['rhs']);
685
686        // dispatch them again to get new literal binding aliases
687        // (This is required by PDO, as no named variable may be used twice)
688        $gp1x = $this->_dispatch($query['lhs']);
689        $gp2x = $this->_dispatch($query['rhs']);
690
691        // determine non-overlapping terms
692        $ta = array_diff($gp1['terms'], $gp2['terms']);
693        $tb = array_diff($gp2['terms'], $gp1['terms']);
694
695        // determine overlapping terms
696        $tc = array_intersect($gp1['terms'], $gp2['terms']);
697
698        // determine final terms
699        $terms = array_unique(array_merge($gp1['terms'], $gp2['terms']));
700
701        // construct selected term list
702        $sa = array_merge($ta, $tb);
703        $sb = array_merge($ta, $tb);
704
705        // append common terms with renaming
706        foreach($tc as $c) {
707            $sa[] = 'r1.'.$c.' AS '.$c;
708            $sb[] = 'r3.'.$c.' AS '.$c;
709        }
710
711        $sa = implode(', ', $sa);
712        $sb = implode(', ', $sb);
713
714        return  array(
715            'sql'=>'SELECT DISTINCT '.$sa.' FROM ('.$gp1['sql'].') r1 LEFT OUTER JOIN ('.$gp2['sql'].') r2 ON (1=0) UNION SELECT DISTINCT '.$sb.' FROM ('.$gp2x['sql'].') r3 LEFT OUTER JOIN ('.$gp1x['sql'].') r4 ON (1=0)',
716            'terms'=>$terms
717        );
718    }
719
720    /**
721     * Translate projection and ordering.
722     */
723    function _trans_select($query) {
724        $gp = $this->_dispatch($query['group']);
725        $vars = $query['projection'];
726        $order = $query['ordering'];
727        $group = $query['grouping'];
728        $consider = $query['considering'];
729        $terms = array();
730        $fields = array();
731
732        // if we get a don't group, put sentinel value in place
733        if($group === false) $group = array();
734
735        // massage ordering to comply with grouping
736        // we do this by combining ordering and sorting information as follows:
737        // The new ordering is {i, Gc, Oc} where
738        //  i = (order intersect group)
739        //  Gc = (group diff order)
740        //  Oc = (order diff group)
741        $order_vars = array();
742        $order_lookup = array();
743        foreach($order as $o) {
744            $order_vars[] = $o['variable'];
745            $order_lookup[$o['variable']] = $o;
746        }
747
748        // determine the three components
749        $order_i = array_intersect($order_vars, $group);
750        $group_c = array_diff($group, $order_vars);
751        $order_c = array_diff($order_vars, $group);
752        $order_n = array_merge($order_i, $group_c, $order_c);
753
754        // construct new ordering array
755        $neworder = array();
756        foreach($order_n as $ovar) {
757            if(!empty($order_lookup[$ovar])) {
758                $neworder[] = $order_lookup[$ovar];
759            } else {
760                $neworder[] = array('variable'=>$ovar, 'direction'=>'asc');
761            }
762        }
763
764        // project extra fields that are required for the grouping
765        foreach($group as $v) {
766            // only project them if they're not projected somewhere else
767            if(!in_array($v, $vars)) {
768                $name = $this->_name(array('type'=>'variable', 'text'=>$v));
769                $fields[] = $name;
770
771                // store grouping translation
772                $this->grouped[$name] = $v;
773            }
774        }
775
776
777        // determine exact projection
778        foreach($vars as $v) {
779            // determine projection translation
780            $name = $this->_name(array('type'=>'variable','text'=>$v));
781
782            // fix projected variable into SQL
783            $terms[] = $name;
784            $fields[] = $name;
785
786            // store projection translation
787            $this->projected[$name] = $v;
788
789            // store grouping translation
790            if(in_array($v, $group)) {
791                $this->grouped[$name] = $v;
792            }
793        }
794
795        // add fields suggested for consideration
796        foreach($consider as $v) {
797            $name = $this->_name(array('type'=>'variable', 'text'=>$v));
798            $alias = $this->_alias('c');
799            $fields[] = "$name AS $alias";
800        }
801
802        // assign ordering if required
803        $ordering = array();
804        foreach($neworder as $o) {
805            $name = $this->_name(array('type'=>'variable','text'=>$o['variable']));
806            $orderTerms = $this->_db->orderBy($name);
807            foreach($orderTerms as $term) {
808                $a = $this->_alias('o');
809                $fields[] = "$term AS $a";
810                $ordering[] = "$a ".($o['direction'] == 'asc'?'ASC':'DESC');
811            }
812        }
813
814        // construct select list
815        $fields = implode(', ',$fields);
816
817        // construct ordering
818        if(count($ordering)>0) {
819            $ordering = ' ORDER BY '.implode(', ',$ordering);
820        } else {
821            $ordering = '';
822        }
823
824        return array(
825            'sql'=>'SELECT DISTINCT '.$fields.' FROM ('.$gp['sql'].') r'.$ordering,
826            'terms'=>$terms
827        );
828    }
829
830    function _dispatch($query) {
831        switch($query['type']) {
832            case 'select':
833                return $this->_trans_select($query);
834            case 'union':
835                return $this->_trans_union($query);
836            case 'minus':
837                return $this->_trans_minus($query);
838            case 'optional':
839                return $this->_trans_opt($query);
840            case 'filter':
841                return $this->_trans_filter($query);
842            case 'triple':
843                return $this->_trans_tp($query);
844            case 'and':
845                return $this->_trans_and($query);
846            default:
847                msg(sprintf($this->getLang('error_triples_node'),hsc($query['type'])),-1);
848                return array('sql'=>'<<INVALID QUERY NODE>>', 'terms'=>array());
849        }
850    }
851
852    /**
853     * Translates an abstract query tree to SQL.
854     */
855    function translate($query) {
856        $q = $this->_dispatch($query);
857        return array($q['sql'], $this->literals, $this->projected, $this->grouped);
858    }
859}
860
861/**
862 * This iterator is used to offer an interface over a
863 * relations query result.
864 */
865class strata_relations_iterator implements Iterator {
866    function __construct($pdostatement, $projection) {
867        // backend iterator
868        $this->data = $pdostatement;
869
870        // state information
871        $this->closed = false;
872        $this->id = 0;
873
874        // projection data
875        $this->projection = $projection;
876
877        // initialize the iterator
878        $this->next();
879    }
880
881    function current() {
882        return $this->row;
883    }
884
885    function key() {
886        return $this->id;
887    }
888
889    function next() {
890        // fetch...
891        $this->row = $this->data->fetch(PDO::FETCH_ASSOC);
892
893        if($this->row) {
894            $row = array();
895
896            // ...project...
897            foreach($this->projection as $alias=>$field) {
898                $row[$field] = $this->row[$alias] != null ? array($this->row[$alias]) : array();
899            }
900            $this->row = $row;
901
902            // ...and increment the id.
903            $this->id++;
904        }
905
906        // Close the backend if we're out of rows.
907        // (This should not be necessary if everyone closes
908        // their iterator after use -- but experience dictates that
909        // this is a good safety net)
910        if(!$this->valid()) {
911            $this->closeCursor();
912        }
913    }
914
915    function rewind() {
916        // noop
917    }
918
919    function valid() {
920        return $this->row != null;
921    }
922
923    /**
924     * Closes this result set.
925     */
926    function closeCursor() {
927        if(!$this->closed) {
928            $this->data->closeCursor();
929            $this->closed = true;
930        }
931    }
932}
933
934/**
935 * This iterator is used to offer an interface over a
936 * resources query result.
937 */
938class strata_resource_iterator implements Iterator {
939    function __construct($relations, $projection) {
940        // backend iterator (ordered by tuple)
941        $this->data = $relations;
942
943        // state information
944        $this->closed = false;
945        $this->valid = true;
946        $this->item = null;
947        $this->subject = null;
948
949        // projection data
950        list($this->__subject, $this->__predicate, $this->__object) = $projection;
951
952        // initialize the iterator
953        $this->next();
954    }
955
956    function current() {
957        return $this->item;
958    }
959
960    function key() {
961        return $this->subject;
962    }
963
964    function next() {
965        if(!$this->data->valid()) {
966            $this->valid = false;
967            return;
968        }
969
970        // the current relation
971        $peekRow = $this->data->current();
972
973        // construct a new subject
974        $this->item = array();
975        $this->subject = $peekRow[$this->__subject][0];
976
977        // continue aggregating data as long as the subject doesn't change and
978        // there is data available
979        while($this->data->valid() && $peekRow[$this->__subject][0] == $this->subject) {
980            $p = $peekRow[$this->__predicate][0];
981            $o = $peekRow[$this->__object][0];
982            if(!isset($this->item[$p])) $this->item[$p] = array();
983            $this->item[$p][] = $o;
984
985            $this->data->next();
986            $peekRow = $this->data->current();
987        }
988
989        return $this->item;
990    }
991
992    function rewind() {
993        // noop
994    }
995
996    function valid() {
997        return $this->valid;
998    }
999
1000    /**
1001     * Closes this result set.
1002     */
1003    function closeCursor() {
1004        if(!$this->closed) {
1005            $this->data->closeCursor();
1006            $this->closed = true;
1007        }
1008    }
1009}
1010
1011/**
1012 * This iterator aggregates the results of the underlying
1013 * iterator for the given grouping key.
1014 */
1015class strata_aggregating_iterator implements Iterator {
1016    function __construct($pdostatement, $projection, $grouped) {
1017        // backend iterator (ordered by tuple)
1018        $this->data = $pdostatement;
1019
1020        // state information
1021        $this->closed = false;
1022        $this->valid = true;
1023        $this->item = null;
1024        $this->subject = 0;
1025
1026        $this->groupKey = $grouped;
1027        $this->projection = $projection;
1028
1029        // initialize the iterator
1030        $this->peekRow = $this->data->fetch(PDO::FETCH_ASSOC);
1031        $this->next();
1032    }
1033
1034    function current() {
1035        return $this->item;
1036    }
1037
1038    function key() {
1039        return $this->subject;
1040    }
1041
1042    private function extractKey($row) {
1043        $result = array();
1044        foreach($this->groupKey as $alias=>$field) {
1045            $result[$field] = $row[$alias];
1046        }
1047        return $result;
1048    }
1049
1050    private function keyCheck($a, $b) {
1051        return $a === $b;
1052    }
1053
1054    function next() {
1055        if($this->peekRow == null) {
1056            $this->valid = false;
1057            return;
1058        }
1059
1060        // the current relation
1061        $key = $this->extractKey($this->peekRow);
1062
1063        // construct a new subject
1064        $this->subject++;
1065        $this->item = array();
1066
1067        // continue aggregating data as long as the subject doesn't change and
1068        // there is data available
1069        while($this->peekRow != null && $this->keyCheck($key,$this->extractKey($this->peekRow))) {
1070            foreach($this->projection as $alias=>$field) {
1071                if(in_array($field, $this->groupKey)) {
1072                    // it is a key field, grab it directly from the key
1073                    $this->item[$field] = $key[$field]!=null ? array($key[$field]) : array();
1074                } else {
1075                    // lazy create the field's bucket
1076                    if(empty($this->item[$field])) {
1077                        $this->item[$field] = array();
1078                    }
1079
1080                    // push the item into the bucket if we have an item
1081                    if($this->peekRow[$alias] != null) {
1082                        $this->item[$field][] = $this->peekRow[$alias];
1083                    }
1084                }
1085            }
1086
1087            $this->peekRow = $this->data->fetch(PDO::FETCH_ASSOC);
1088        }
1089
1090        if($this->peekRow == null) {
1091            $this->closeCursor();
1092        }
1093
1094        return $this->item;
1095    }
1096
1097    function rewind() {
1098        // noop
1099    }
1100
1101    function valid() {
1102        return $this->valid;
1103    }
1104
1105    /**
1106     * Closes this result set.
1107     */
1108    function closeCursor() {
1109        if(!$this->closed) {
1110            $this->data->closeCursor();
1111            $this->closed = true;
1112        }
1113    }
1114}
1115