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