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