xref: /plugin/struct/meta/QueryBuilder.php (revision 748e747f37aa44250ee32847b5fc3ff1e47f0835)
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
128c551fd7SAndreas Gohr    /** @var array placeholder -> values */
138c551fd7SAndreas Gohr    protected $values = array();
148c551fd7SAndreas Gohr    /** @var array (alias -> statement */
158c551fd7SAndreas Gohr    protected $select = array();
168c551fd7SAndreas Gohr    /** @var array (alias -> statement) */
178c551fd7SAndreas Gohr    protected $from = array();
182f68434dSAndreas Gohr    /** @var array (alias -> "table"|"join") keeps how tables were added, as table or join */
192f68434dSAndreas Gohr    protected $type = array();
208c551fd7SAndreas Gohr    /** @var QueryBuilderWhere */
218c551fd7SAndreas Gohr    protected $where;
2283cb959bSAndreas Gohr    /** @var  string[] */
232f68434dSAndreas Gohr    protected $orderby = array();
2483cb959bSAndreas Gohr    /** @var  string[] */
252f68434dSAndreas Gohr    protected $groupby = array();
268c551fd7SAndreas Gohr
278c551fd7SAndreas Gohr    /**
288c551fd7SAndreas Gohr     * QueryBuilder constructor.
298c551fd7SAndreas Gohr     */
30d6d97f60SAnna Dabrowska    public function __construct()
31d6d97f60SAnna Dabrowska    {
32af993d55SMichael Grosse        $this->where = new QueryBuilderWhere($this);
338c551fd7SAndreas Gohr    }
348c551fd7SAndreas Gohr
358c551fd7SAndreas Gohr    /**
368c551fd7SAndreas Gohr     * Adds a column to select
378c551fd7SAndreas Gohr     *
388c551fd7SAndreas Gohr     * If the alias already exists, the current statement for that alias will be overwritten.
398c551fd7SAndreas Gohr     *
408c551fd7SAndreas Gohr     * @param string $tablealias The table to select from
418c551fd7SAndreas Gohr     * @param string $column The column to select
428c551fd7SAndreas Gohr     * @param string $alias Under whichname to slect the column. blank for column name
438c551fd7SAndreas Gohr     */
44d6d97f60SAnna Dabrowska    public function addSelectColumn($tablealias, $column, $alias = '')
45d6d97f60SAnna Dabrowska    {
468c551fd7SAndreas Gohr        if ($alias === '') $alias = $column;
47aa1f5074SAndreas Gohr        if (!isset($this->from[$tablealias])) {
48aa1f5074SAndreas Gohr            throw new StructException('Table Alias does not exist');
49aa1f5074SAndreas Gohr        }
509ee8a979SAndreas Gohr        $this->select[$alias] = "$tablealias.$column";
518c551fd7SAndreas Gohr    }
528c551fd7SAndreas Gohr
538c551fd7SAndreas Gohr    /**
549ee8a979SAndreas Gohr     * Add a new column selection statement
558c551fd7SAndreas Gohr     *
569ee8a979SAndreas Gohr     * Basically the same as @see addSelectColumn() but accepts any statement. This is useful to
578c551fd7SAndreas Gohr     * select things like fixed strings or more complex function calls, but the correctness will not
588c551fd7SAndreas Gohr     * be checked.
598c551fd7SAndreas Gohr     *
608c551fd7SAndreas Gohr     * If the alias already exists, the current statement for that alias will be overwritten.
618c551fd7SAndreas Gohr     *
628c551fd7SAndreas Gohr     * @param string $statement
638c551fd7SAndreas Gohr     * @param string $alias
648c551fd7SAndreas Gohr     */
65d6d97f60SAnna Dabrowska    public function addSelectStatement($statement, $alias)
66d6d97f60SAnna Dabrowska    {
679ee8a979SAndreas Gohr        $this->select[$alias] = $statement;
689ee8a979SAndreas Gohr    }
699ee8a979SAndreas Gohr
709ee8a979SAndreas Gohr    /**
719ee8a979SAndreas Gohr     * Return an already defined column selection statement based on the alias
729ee8a979SAndreas Gohr     *
739ee8a979SAndreas Gohr     * @param string $alias
749ee8a979SAndreas Gohr     * @return string
759ee8a979SAndreas Gohr     * @throws StructException when the alias does not exist
769ee8a979SAndreas Gohr     */
77d6d97f60SAnna Dabrowska    public function getSelectStatement($alias)
78d6d97f60SAnna Dabrowska    {
799ee8a979SAndreas Gohr        if (!isset($this->select[$alias])) {
809ee8a979SAndreas Gohr            throw new StructException('No such select alias');
819ee8a979SAndreas Gohr        }
829ee8a979SAndreas Gohr
839ee8a979SAndreas Gohr        return $this->select[$alias];
848c551fd7SAndreas Gohr    }
858c551fd7SAndreas Gohr
868c551fd7SAndreas Gohr    /**
878c551fd7SAndreas Gohr     * Adds the the table to the FROM statement part
888c551fd7SAndreas Gohr     *
898c551fd7SAndreas Gohr     * @param string $table the table to add
908c551fd7SAndreas Gohr     * @param string $alias alias for the table, blank for table name
918c551fd7SAndreas Gohr     */
92d6d97f60SAnna Dabrowska    public function addTable($table, $alias = '')
93d6d97f60SAnna Dabrowska    {
948c551fd7SAndreas Gohr        if ($alias === '') $alias = $table;
95aa1f5074SAndreas Gohr        if (isset($this->from[$alias])) {
96aa1f5074SAndreas Gohr            throw new StructException('Table Alias exists');
97aa1f5074SAndreas Gohr        }
988c551fd7SAndreas Gohr        $this->from[$alias] = "$table AS $alias";
992f68434dSAndreas Gohr        $this->type[$alias] = 'table';
1008c551fd7SAndreas Gohr    }
1018c551fd7SAndreas Gohr
1028c551fd7SAndreas Gohr    /**
1038c551fd7SAndreas Gohr     * Adds a LEFT JOIN clause to the FROM statement part, sorted at the correct spot
1048c551fd7SAndreas Gohr     *
1058c551fd7SAndreas Gohr     * @param string $leftalias the alias of the left table you're joining on, has to exist already
1068c551fd7SAndreas Gohr     * @param string $righttable the right table to be joined
1078c551fd7SAndreas Gohr     * @param string $rightalias an alias for the right table, blank for table name
1088c551fd7SAndreas Gohr     * @param string $onclause the ON clause condition the join is based on
1098c551fd7SAndreas Gohr     */
110d6d97f60SAnna Dabrowska    public function addLeftJoin($leftalias, $righttable, $rightalias, $onclause)
111d6d97f60SAnna Dabrowska    {
1128c551fd7SAndreas Gohr        if ($rightalias === '') $rightalias = $righttable;
113aa1f5074SAndreas Gohr        if (!isset($this->from[$leftalias])) {
114aa1f5074SAndreas Gohr            throw new StructException('Table Alias does not exist');
115aa1f5074SAndreas Gohr        }
116aa1f5074SAndreas Gohr        if (isset($this->from[$rightalias])) {
117aa1f5074SAndreas Gohr            throw new StructException('Table Alias already exists');
118aa1f5074SAndreas Gohr        }
1198c551fd7SAndreas Gohr
1208c551fd7SAndreas Gohr        $pos = array_search($leftalias, array_keys($this->from));
1218c551fd7SAndreas Gohr        $statement = "LEFT OUTER JOIN $righttable AS $rightalias ON $onclause";
122*748e747fSAnna Dabrowska        $this->from = $this->arrayInsert($this->from, array($rightalias => $statement), $pos + 1);
1232f68434dSAndreas Gohr        $this->type[$rightalias] = 'join';
1248c551fd7SAndreas Gohr    }
1258c551fd7SAndreas Gohr
1268c551fd7SAndreas Gohr    /**
1278c551fd7SAndreas Gohr     * Returns the current WHERE filters and allows to set new ones
1288c551fd7SAndreas Gohr     *
1298c551fd7SAndreas Gohr     * @return QueryBuilderWhere
1308c551fd7SAndreas Gohr     */
131d6d97f60SAnna Dabrowska    public function filters()
132d6d97f60SAnna Dabrowska    {
1338c551fd7SAndreas Gohr        return $this->where;
1348c551fd7SAndreas Gohr    }
1358c551fd7SAndreas Gohr
1368c551fd7SAndreas Gohr    /**
13783cb959bSAndreas Gohr     * Add an ORDER BY clause
13883cb959bSAndreas Gohr     *
13983cb959bSAndreas Gohr     * @param string $sort a single sorting condition
14083cb959bSAndreas Gohr     */
141d6d97f60SAnna Dabrowska    public function addOrderBy($sort)
142d6d97f60SAnna Dabrowska    {
14383cb959bSAndreas Gohr        $this->orderby[] = $sort;
14483cb959bSAndreas Gohr    }
14583cb959bSAndreas Gohr
14683cb959bSAndreas Gohr    /**
14783cb959bSAndreas Gohr     * Add an GROUP BY clause
14883cb959bSAndreas Gohr     *
1492f68434dSAndreas Gohr     * @param string $tablealias
1502f68434dSAndreas Gohr     * @param string $column
15183cb959bSAndreas Gohr     */
152d6d97f60SAnna Dabrowska    public function addGroupByColumn($tablealias, $column)
153d6d97f60SAnna Dabrowska    {
154aa1f5074SAndreas Gohr        if (!isset($this->from[$tablealias])) {
155aa1f5074SAndreas Gohr            throw new StructException('Table Alias does not exist');
156aa1f5074SAndreas Gohr        }
1572f68434dSAndreas Gohr        $this->groupby[] = "$tablealias.$column";
1582f68434dSAndreas Gohr    }
1592f68434dSAndreas Gohr
1602f68434dSAndreas Gohr    /**
1612f68434dSAndreas Gohr     * Add an GROUP BY clause
1622f68434dSAndreas Gohr     *
1632f68434dSAndreas Gohr     * Like @see addGroupByColumn but accepts an arbitrary statement
1642f68434dSAndreas Gohr     *
1652f68434dSAndreas Gohr     * @param string $statement a single grouping clause
1662f68434dSAndreas Gohr     */
167d6d97f60SAnna Dabrowska    public function addGroupByStatement($statement)
168d6d97f60SAnna Dabrowska    {
1692f68434dSAndreas Gohr        $this->groupby[] = $statement;
17083cb959bSAndreas Gohr    }
17183cb959bSAndreas Gohr
17283cb959bSAndreas Gohr    /**
1738c551fd7SAndreas Gohr     * Adds a value to the statement
1748c551fd7SAndreas Gohr     *
175df30dbf7SAndreas Gohr     * This function returns the name of the placeholder you have to use in your statement, so whenever
176df30dbf7SAndreas Gohr     * you need to use a user value in a statement, call this first, then add the statement through the
177df30dbf7SAndreas Gohr     * other functions using the returned placeholder.
1788c551fd7SAndreas Gohr     *
1798c551fd7SAndreas Gohr     * @param mixed $value
1808c551fd7SAndreas Gohr     * @return string
1818c551fd7SAndreas Gohr     */
182d6d97f60SAnna Dabrowska    public function addValue($value)
183d6d97f60SAnna Dabrowska    {
1848c551fd7SAndreas Gohr        static $count = 0;
1858c551fd7SAndreas Gohr        $count++;
1868c551fd7SAndreas Gohr
1878c551fd7SAndreas Gohr        $placeholder = ":!!val$count!!:"; // sqlite plugin does not support named parameters, yet so we have simulate it
1888c551fd7SAndreas Gohr        $this->values[$placeholder] = $value;
1898c551fd7SAndreas Gohr        return $placeholder;
1908c551fd7SAndreas Gohr    }
1918c551fd7SAndreas Gohr
1928c551fd7SAndreas Gohr    /**
1938c551fd7SAndreas Gohr     * Creates a new table alias that has not been used before
1948c551fd7SAndreas Gohr     *
195db7970caSAndreas Gohr     * @param string $prefix the prefix for the alias, helps with readability of the SQL
1968c551fd7SAndreas Gohr     * @return string
1978c551fd7SAndreas Gohr     */
198d6d97f60SAnna Dabrowska    public function generateTableAlias($prefix = 'T')
199d6d97f60SAnna Dabrowska    {
2008c551fd7SAndreas Gohr        static $count = 0;
2018c551fd7SAndreas Gohr        $count++;
202db7970caSAndreas Gohr        return $prefix . $count;
2038c551fd7SAndreas Gohr    }
2048c551fd7SAndreas Gohr
2058c551fd7SAndreas Gohr    /**
2068c551fd7SAndreas Gohr     * Returns the complete SQL statement and the values to apply
2078c551fd7SAndreas Gohr     *
20883cb959bSAndreas Gohr     * @return array ($sql, $vals)
2098c551fd7SAndreas Gohr     */
210d6d97f60SAnna Dabrowska    public function getSQL()
211d6d97f60SAnna Dabrowska    {
2122f68434dSAndreas Gohr        // FROM needs commas only for tables, not joins
2132f68434dSAndreas Gohr        $from = '';
2142f68434dSAndreas Gohr        foreach ($this->from as $alias => $statement) {
2152f68434dSAndreas Gohr            if ($this->type[$alias] == 'table' && $from) {
2162f68434dSAndreas Gohr                $from .= ",\n";
2172f68434dSAndreas Gohr            } else {
2182f68434dSAndreas Gohr                $from .= "\n";
2192f68434dSAndreas Gohr            }
2202f68434dSAndreas Gohr
2212f68434dSAndreas Gohr            $from .= $statement;
2222f68434dSAndreas Gohr        }
2232f68434dSAndreas Gohr
2249ee8a979SAndreas Gohr        // prepare aliases for the select columns
2259ee8a979SAndreas Gohr        $selects = array();
2269ee8a979SAndreas Gohr        foreach ($this->select as $alias => $select) {
2279ee8a979SAndreas Gohr            $selects[] = "$select AS $alias";
2289ee8a979SAndreas Gohr        }
2299ee8a979SAndreas Gohr
2308c551fd7SAndreas Gohr        $sql =
2319ee8a979SAndreas Gohr            ' SELECT ' . join(",\n", $selects) . "\n" .
2322f68434dSAndreas Gohr            '   FROM ' . $from . "\n" .
2338c551fd7SAndreas Gohr            '  WHERE ' . $this->where->toSQL() . "\n";
2348c551fd7SAndreas Gohr
23583cb959bSAndreas Gohr        if ($this->groupby) {
23683cb959bSAndreas Gohr            $sql .=
23783cb959bSAndreas Gohr                'GROUP BY ' . join(",\n", $this->groupby) . "\n";
23883cb959bSAndreas Gohr        }
23983cb959bSAndreas Gohr
2402f68434dSAndreas Gohr        if ($this->orderby) {
2412f68434dSAndreas Gohr            $sql .=
2422f68434dSAndreas Gohr                'ORDER BY ' . join(",\n", $this->orderby) . "\n";
2432f68434dSAndreas Gohr        }
2442f68434dSAndreas Gohr
2458c551fd7SAndreas Gohr        return $this->fixPlaceholders($sql);
2468c551fd7SAndreas Gohr    }
2478c551fd7SAndreas Gohr
2488c551fd7SAndreas Gohr    /**
2498c551fd7SAndreas Gohr     * Replaces the named placeholders with ? placeholders
2508c551fd7SAndreas Gohr     *
2518c551fd7SAndreas Gohr     * Until the sqlite plugin can use named placeholder properly
2528c551fd7SAndreas Gohr     *
2538c551fd7SAndreas Gohr     * @param string $sql
2548c551fd7SAndreas Gohr     * @return array
2558c551fd7SAndreas Gohr     */
256d6d97f60SAnna Dabrowska    protected function fixPlaceholders($sql)
257d6d97f60SAnna Dabrowska    {
2588c551fd7SAndreas Gohr        $vals = array();
2598c551fd7SAndreas Gohr
2608c551fd7SAndreas Gohr        while (preg_match('/(:!!val\d+!!:)/', $sql, $m)) {
2618c551fd7SAndreas Gohr            $pl = $m[1];
2622b5df26eSAndreas Gohr
26345664274SAndreas Gohr            if (!array_key_exists($pl, $this->values)) {
264aa1f5074SAndreas Gohr                throw new StructException('Placeholder not found');
265aa1f5074SAndreas Gohr            }
2662b5df26eSAndreas Gohr
2672b5df26eSAndreas Gohr            $sql = preg_replace("/$pl/", '?', $sql, 1);
2688c551fd7SAndreas Gohr            $vals[] = $this->values[$pl];
2698c551fd7SAndreas Gohr        }
2708c551fd7SAndreas Gohr
2718c551fd7SAndreas Gohr        return array($sql, $vals);
2728c551fd7SAndreas Gohr    }
2738c551fd7SAndreas Gohr
2748c551fd7SAndreas Gohr    /**
2758c551fd7SAndreas Gohr     * Insert an array into another array at a given position in an associative array
2768c551fd7SAndreas Gohr     *
2778c551fd7SAndreas Gohr     * @param array $array The initial array
2788c551fd7SAndreas Gohr     * @param array $pairs The array to insert
2798c551fd7SAndreas Gohr     * @param string $key_pos The position at which to insert
2808c551fd7SAndreas Gohr     * @link https://gist.github.com/scribu/588429 simplified
2818c551fd7SAndreas Gohr     * @return array
2828c551fd7SAndreas Gohr     */
283*748e747fSAnna Dabrowska    protected function arrayInsert($array, $pairs, $key_pos)
284d6d97f60SAnna Dabrowska    {
2858c551fd7SAndreas Gohr        $result = array_slice($array, 0, $key_pos);
2868c551fd7SAndreas Gohr        $result = array_merge($result, $pairs);
2878c551fd7SAndreas Gohr        $result = array_merge($result, array_slice($array, $key_pos));
2888c551fd7SAndreas Gohr        return $result;
2898c551fd7SAndreas Gohr    }
2908c551fd7SAndreas Gohr}
291