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