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