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