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 __construct($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