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