18c551fd7SAndreas Gohr<?php 28c551fd7SAndreas Gohr 38c551fd7SAndreas Gohrnamespace dokuwiki\plugin\struct\meta; 48c551fd7SAndreas Gohr 58c551fd7SAndreas Gohr/** 68c551fd7SAndreas Gohr * Class QueryBuilder 78c551fd7SAndreas Gohr * @package dokuwiki\plugin\struct\meta 88c551fd7SAndreas Gohr */ 9d6d97f60SAnna Dabrowskaclass QueryBuilder 10d6d97f60SAnna Dabrowska{ 118c551fd7SAndreas Gohr /** @var array placeholder -> values */ 12*7234bfb1Ssplitbrain protected $values = []; 138c551fd7SAndreas Gohr /** @var array (alias -> statement */ 14*7234bfb1Ssplitbrain protected $select = []; 158c551fd7SAndreas Gohr /** @var array (alias -> statement) */ 16*7234bfb1Ssplitbrain protected $from = []; 172f68434dSAndreas Gohr /** @var array (alias -> "table"|"join") keeps how tables were added, as table or join */ 18*7234bfb1Ssplitbrain protected $type = []; 198c551fd7SAndreas Gohr /** @var QueryBuilderWhere */ 208c551fd7SAndreas Gohr protected $where; 2183cb959bSAndreas Gohr /** @var string[] */ 22*7234bfb1Ssplitbrain protected $orderby = []; 2383cb959bSAndreas Gohr /** @var string[] */ 24*7234bfb1Ssplitbrain protected $groupby = []; 258c551fd7SAndreas Gohr 268c551fd7SAndreas Gohr /** 278c551fd7SAndreas Gohr * QueryBuilder constructor. 288c551fd7SAndreas Gohr */ 29d6d97f60SAnna Dabrowska public function __construct() 30d6d97f60SAnna Dabrowska { 31af993d55SMichael Grosse $this->where = new QueryBuilderWhere($this); 328c551fd7SAndreas Gohr } 338c551fd7SAndreas Gohr 348c551fd7SAndreas Gohr /** 358c551fd7SAndreas Gohr * Adds a column to select 368c551fd7SAndreas Gohr * 378c551fd7SAndreas Gohr * If the alias already exists, the current statement for that alias will be overwritten. 388c551fd7SAndreas Gohr * 398c551fd7SAndreas Gohr * @param string $tablealias The table to select from 408c551fd7SAndreas Gohr * @param string $column The column to select 418c551fd7SAndreas Gohr * @param string $alias Under whichname to slect the column. blank for column name 428c551fd7SAndreas Gohr */ 43d6d97f60SAnna Dabrowska public function addSelectColumn($tablealias, $column, $alias = '') 44d6d97f60SAnna Dabrowska { 458c551fd7SAndreas Gohr if ($alias === '') $alias = $column; 46aa1f5074SAndreas Gohr if (!isset($this->from[$tablealias])) { 47aa1f5074SAndreas Gohr throw new StructException('Table Alias does not exist'); 48aa1f5074SAndreas Gohr } 499ee8a979SAndreas Gohr $this->select[$alias] = "$tablealias.$column"; 508c551fd7SAndreas Gohr } 518c551fd7SAndreas Gohr 528c551fd7SAndreas Gohr /** 539ee8a979SAndreas Gohr * Add a new column selection statement 548c551fd7SAndreas Gohr * 550549dcc5SAndreas Gohr * Basically the same as @param string $statement 560549dcc5SAndreas Gohr * @param string $alias 570549dcc5SAndreas Gohr * @see addSelectColumn() but accepts any statement. This is useful to 588c551fd7SAndreas Gohr * select things like fixed strings or more complex function calls, but the correctness will not 598c551fd7SAndreas Gohr * be checked. 608c551fd7SAndreas Gohr * 618c551fd7SAndreas Gohr * If the alias already exists, the current statement for that alias will be overwritten. 628c551fd7SAndreas Gohr * 638c551fd7SAndreas Gohr */ 64d6d97f60SAnna Dabrowska public function addSelectStatement($statement, $alias) 65d6d97f60SAnna Dabrowska { 669ee8a979SAndreas Gohr $this->select[$alias] = $statement; 679ee8a979SAndreas Gohr } 689ee8a979SAndreas Gohr 699ee8a979SAndreas Gohr /** 709ee8a979SAndreas Gohr * Return an already defined column selection statement based on the alias 719ee8a979SAndreas Gohr * 729ee8a979SAndreas Gohr * @param string $alias 739ee8a979SAndreas Gohr * @return string 749ee8a979SAndreas Gohr * @throws StructException when the alias does not exist 759ee8a979SAndreas Gohr */ 76d6d97f60SAnna Dabrowska public function getSelectStatement($alias) 77d6d97f60SAnna Dabrowska { 789ee8a979SAndreas Gohr if (!isset($this->select[$alias])) { 799ee8a979SAndreas Gohr throw new StructException('No such select alias'); 809ee8a979SAndreas Gohr } 819ee8a979SAndreas Gohr 829ee8a979SAndreas Gohr return $this->select[$alias]; 838c551fd7SAndreas Gohr } 848c551fd7SAndreas Gohr 858c551fd7SAndreas Gohr /** 868c551fd7SAndreas Gohr * Adds the the table to the FROM statement part 878c551fd7SAndreas Gohr * 888c551fd7SAndreas Gohr * @param string $table the table to add 898c551fd7SAndreas Gohr * @param string $alias alias for the table, blank for table name 908c551fd7SAndreas Gohr */ 91d6d97f60SAnna Dabrowska public function addTable($table, $alias = '') 92d6d97f60SAnna Dabrowska { 938c551fd7SAndreas Gohr if ($alias === '') $alias = $table; 94aa1f5074SAndreas Gohr if (isset($this->from[$alias])) { 95aa1f5074SAndreas Gohr throw new StructException('Table Alias exists'); 96aa1f5074SAndreas Gohr } 978c551fd7SAndreas Gohr $this->from[$alias] = "$table AS $alias"; 982f68434dSAndreas Gohr $this->type[$alias] = 'table'; 998c551fd7SAndreas Gohr } 1008c551fd7SAndreas Gohr 1018c551fd7SAndreas Gohr /** 1028c551fd7SAndreas Gohr * Adds a LEFT JOIN clause to the FROM statement part, sorted at the correct spot 1038c551fd7SAndreas Gohr * 1048c551fd7SAndreas Gohr * @param string $leftalias the alias of the left table you're joining on, has to exist already 1058c551fd7SAndreas Gohr * @param string $righttable the right table to be joined 1068c551fd7SAndreas Gohr * @param string $rightalias an alias for the right table, blank for table name 1078c551fd7SAndreas Gohr * @param string $onclause the ON clause condition the join is based on 1088c551fd7SAndreas Gohr */ 109d6d97f60SAnna Dabrowska public function addLeftJoin($leftalias, $righttable, $rightalias, $onclause) 110d6d97f60SAnna Dabrowska { 1118c551fd7SAndreas Gohr if ($rightalias === '') $rightalias = $righttable; 112aa1f5074SAndreas Gohr if (!isset($this->from[$leftalias])) { 113aa1f5074SAndreas Gohr throw new StructException('Table Alias does not exist'); 114aa1f5074SAndreas Gohr } 115aa1f5074SAndreas Gohr if (isset($this->from[$rightalias])) { 116aa1f5074SAndreas Gohr throw new StructException('Table Alias already exists'); 117aa1f5074SAndreas Gohr } 1188c551fd7SAndreas Gohr 1198c551fd7SAndreas Gohr $pos = array_search($leftalias, array_keys($this->from)); 1208c551fd7SAndreas Gohr $statement = "LEFT OUTER JOIN $righttable AS $rightalias ON $onclause"; 121*7234bfb1Ssplitbrain $this->from = $this->arrayInsert($this->from, [$rightalias => $statement], $pos + 1); 1222f68434dSAndreas Gohr $this->type[$rightalias] = 'join'; 1238c551fd7SAndreas Gohr } 1248c551fd7SAndreas Gohr 1258c551fd7SAndreas Gohr /** 1268c551fd7SAndreas Gohr * Returns the current WHERE filters and allows to set new ones 1278c551fd7SAndreas Gohr * 1288c551fd7SAndreas Gohr * @return QueryBuilderWhere 1298c551fd7SAndreas Gohr */ 130d6d97f60SAnna Dabrowska public function filters() 131d6d97f60SAnna Dabrowska { 1328c551fd7SAndreas Gohr return $this->where; 1338c551fd7SAndreas Gohr } 1348c551fd7SAndreas Gohr 1358c551fd7SAndreas Gohr /** 13683cb959bSAndreas Gohr * Add an ORDER BY clause 13783cb959bSAndreas Gohr * 13883cb959bSAndreas Gohr * @param string $sort a single sorting condition 13983cb959bSAndreas Gohr */ 140d6d97f60SAnna Dabrowska public function addOrderBy($sort) 141d6d97f60SAnna Dabrowska { 14283cb959bSAndreas Gohr $this->orderby[] = $sort; 14383cb959bSAndreas Gohr } 14483cb959bSAndreas Gohr 14583cb959bSAndreas Gohr /** 14683cb959bSAndreas Gohr * Add an GROUP BY clause 14783cb959bSAndreas Gohr * 1482f68434dSAndreas Gohr * @param string $tablealias 1492f68434dSAndreas Gohr * @param string $column 15083cb959bSAndreas Gohr */ 151d6d97f60SAnna Dabrowska public function addGroupByColumn($tablealias, $column) 152d6d97f60SAnna Dabrowska { 153aa1f5074SAndreas Gohr if (!isset($this->from[$tablealias])) { 154aa1f5074SAndreas Gohr throw new StructException('Table Alias does not exist'); 155aa1f5074SAndreas Gohr } 1562f68434dSAndreas Gohr $this->groupby[] = "$tablealias.$column"; 1572f68434dSAndreas Gohr } 1582f68434dSAndreas Gohr 1592f68434dSAndreas Gohr /** 1602f68434dSAndreas Gohr * Add an GROUP BY clause 1612f68434dSAndreas Gohr * 1620549dcc5SAndreas Gohr * Like @param string $statement a single grouping clause 1630549dcc5SAndreas Gohr * @see addGroupByColumn but accepts an arbitrary statement 1642f68434dSAndreas Gohr * 1652f68434dSAndreas Gohr */ 166d6d97f60SAnna Dabrowska public function addGroupByStatement($statement) 167d6d97f60SAnna Dabrowska { 1682f68434dSAndreas Gohr $this->groupby[] = $statement; 16983cb959bSAndreas Gohr } 17083cb959bSAndreas Gohr 17183cb959bSAndreas Gohr /** 1728c551fd7SAndreas Gohr * Adds a value to the statement 1738c551fd7SAndreas Gohr * 174df30dbf7SAndreas Gohr * This function returns the name of the placeholder you have to use in your statement, so whenever 175df30dbf7SAndreas Gohr * you need to use a user value in a statement, call this first, then add the statement through the 176df30dbf7SAndreas Gohr * other functions using the returned placeholder. 1778c551fd7SAndreas Gohr * 1788c551fd7SAndreas Gohr * @param mixed $value 1798c551fd7SAndreas Gohr * @return string 1808c551fd7SAndreas Gohr */ 181d6d97f60SAnna Dabrowska public function addValue($value) 182d6d97f60SAnna Dabrowska { 1838c551fd7SAndreas Gohr static $count = 0; 1848c551fd7SAndreas Gohr $count++; 1858c551fd7SAndreas Gohr 1868c551fd7SAndreas Gohr $placeholder = ":!!val$count!!:"; // sqlite plugin does not support named parameters, yet so we have simulate it 1878c551fd7SAndreas Gohr $this->values[$placeholder] = $value; 1888c551fd7SAndreas Gohr return $placeholder; 1898c551fd7SAndreas Gohr } 1908c551fd7SAndreas Gohr 1918c551fd7SAndreas Gohr /** 1928c551fd7SAndreas Gohr * Creates a new table alias that has not been used before 1938c551fd7SAndreas Gohr * 194db7970caSAndreas Gohr * @param string $prefix the prefix for the alias, helps with readability of the SQL 1958c551fd7SAndreas Gohr * @return string 1968c551fd7SAndreas Gohr */ 197d6d97f60SAnna Dabrowska public function generateTableAlias($prefix = 'T') 198d6d97f60SAnna Dabrowska { 1998c551fd7SAndreas Gohr static $count = 0; 2008c551fd7SAndreas Gohr $count++; 201db7970caSAndreas Gohr return $prefix . $count; 2028c551fd7SAndreas Gohr } 2038c551fd7SAndreas Gohr 2048c551fd7SAndreas Gohr /** 2058c551fd7SAndreas Gohr * Returns the complete SQL statement and the values to apply 2068c551fd7SAndreas Gohr * 20783cb959bSAndreas Gohr * @return array ($sql, $vals) 2088c551fd7SAndreas Gohr */ 209d6d97f60SAnna Dabrowska public function getSQL() 210d6d97f60SAnna Dabrowska { 2112f68434dSAndreas Gohr // FROM needs commas only for tables, not joins 2122f68434dSAndreas Gohr $from = ''; 2132f68434dSAndreas Gohr foreach ($this->from as $alias => $statement) { 2142f68434dSAndreas Gohr if ($this->type[$alias] == 'table' && $from) { 2152f68434dSAndreas Gohr $from .= ",\n"; 2162f68434dSAndreas Gohr } else { 2172f68434dSAndreas Gohr $from .= "\n"; 2182f68434dSAndreas Gohr } 2192f68434dSAndreas Gohr 2202f68434dSAndreas Gohr $from .= $statement; 2212f68434dSAndreas Gohr } 2222f68434dSAndreas Gohr 2239ee8a979SAndreas Gohr // prepare aliases for the select columns 224*7234bfb1Ssplitbrain $selects = []; 2259ee8a979SAndreas Gohr foreach ($this->select as $alias => $select) { 2269ee8a979SAndreas Gohr $selects[] = "$select AS $alias"; 2279ee8a979SAndreas Gohr } 2289ee8a979SAndreas Gohr 2298c551fd7SAndreas Gohr $sql = 230*7234bfb1Ssplitbrain ' SELECT ' . implode(",\n", $selects) . "\n" . 2312f68434dSAndreas Gohr ' FROM ' . $from . "\n" . 2328c551fd7SAndreas Gohr ' WHERE ' . $this->where->toSQL() . "\n"; 2338c551fd7SAndreas Gohr 23483cb959bSAndreas Gohr if ($this->groupby) { 23583cb959bSAndreas Gohr $sql .= 236*7234bfb1Ssplitbrain 'GROUP BY ' . implode(",\n", $this->groupby) . "\n"; 23783cb959bSAndreas Gohr } 23883cb959bSAndreas Gohr 2392f68434dSAndreas Gohr if ($this->orderby) { 2402f68434dSAndreas Gohr $sql .= 241*7234bfb1Ssplitbrain 'ORDER BY ' . implode(",\n", $this->orderby) . "\n"; 2422f68434dSAndreas Gohr } 2432f68434dSAndreas Gohr 2448c551fd7SAndreas Gohr return $this->fixPlaceholders($sql); 2458c551fd7SAndreas Gohr } 2468c551fd7SAndreas Gohr 2478c551fd7SAndreas Gohr /** 2488c551fd7SAndreas Gohr * Replaces the named placeholders with ? placeholders 2498c551fd7SAndreas Gohr * 2508c551fd7SAndreas Gohr * Until the sqlite plugin can use named placeholder properly 2518c551fd7SAndreas Gohr * 2528c551fd7SAndreas Gohr * @param string $sql 2538c551fd7SAndreas Gohr * @return array 2548c551fd7SAndreas Gohr */ 255d6d97f60SAnna Dabrowska protected function fixPlaceholders($sql) 256d6d97f60SAnna Dabrowska { 257*7234bfb1Ssplitbrain $vals = []; 2588c551fd7SAndreas Gohr 2598c551fd7SAndreas Gohr while (preg_match('/(:!!val\d+!!:)/', $sql, $m)) { 2608c551fd7SAndreas Gohr $pl = $m[1]; 2612b5df26eSAndreas Gohr 26245664274SAndreas Gohr if (!array_key_exists($pl, $this->values)) { 263aa1f5074SAndreas Gohr throw new StructException('Placeholder not found'); 264aa1f5074SAndreas Gohr } 2652b5df26eSAndreas Gohr 2662b5df26eSAndreas Gohr $sql = preg_replace("/$pl/", '?', $sql, 1); 2678c551fd7SAndreas Gohr $vals[] = $this->values[$pl]; 2688c551fd7SAndreas Gohr } 2698c551fd7SAndreas Gohr 270*7234bfb1Ssplitbrain return [$sql, $vals]; 2718c551fd7SAndreas Gohr } 2728c551fd7SAndreas Gohr 2738c551fd7SAndreas Gohr /** 2748c551fd7SAndreas Gohr * Insert an array into another array at a given position in an associative array 2758c551fd7SAndreas Gohr * 2768c551fd7SAndreas Gohr * @param array $array The initial array 2778c551fd7SAndreas Gohr * @param array $pairs The array to insert 2788c551fd7SAndreas Gohr * @param string $key_pos The position at which to insert 2798c551fd7SAndreas Gohr * @return array 2800549dcc5SAndreas Gohr * @link https://gist.github.com/scribu/588429 simplified 2818c551fd7SAndreas Gohr */ 282748e747fSAnna Dabrowska protected function arrayInsert($array, $pairs, $key_pos) 283d6d97f60SAnna Dabrowska { 2848c551fd7SAndreas Gohr $result = array_slice($array, 0, $key_pos); 2858c551fd7SAndreas Gohr $result = array_merge($result, $pairs); 2868c551fd7SAndreas Gohr $result = array_merge($result, array_slice($array, $key_pos)); 2878c551fd7SAndreas Gohr return $result; 2888c551fd7SAndreas Gohr } 2898c551fd7SAndreas Gohr} 290