xref: /plugin/struct/meta/SearchSQLBuilder.php (revision cfefffa4d2c67e8227ac2e612157363595988ec5)
1<?php
2
3namespace dokuwiki\plugin\struct\meta;
4
5/**
6 * Creates the SQL query using the QueryBuilder
7 *
8 * @internal This class is used by the Search class and should probably never be used directly
9 */
10class SearchSQLBuilder
11{
12    /** @var QueryBuilder */
13    protected $qb;
14
15    /** @var bool Include latest = 1 in select query */
16    protected $selectLatest = true;
17
18    /**
19     * SearchSQLBuilder constructor.
20     */
21    public function __construct()
22    {
23        $this->qb = new QueryBuilder();
24    }
25
26    /**
27     * Add the schemas to the query
28     *
29     * @param Schema[] $schemas Schema names to query
30     */
31    public function addSchemas($schemas)
32    {
33        // basic tables
34        $first_table = '';
35        foreach ($schemas as $schema) {
36            $datatable = 'data_' . $schema->getTable();
37            if ($first_table) {
38                // follow up tables
39                $this->qb->addLeftJoin($first_table, $datatable, $datatable, "$first_table.pid = $datatable.pid");
40            } else {
41                // first table
42                $this->qb->addTable($datatable);
43
44                // add conditional page clauses if pid has a value
45                $subAnd = $this->qb->filters()->whereSubAnd();
46                $subAnd->whereAnd("$datatable.pid = ''");
47                $subOr = $subAnd->whereSubOr();
48                $subOr->whereAnd("GETACCESSLEVEL($datatable.pid) > 0");
49                $subOr->whereAnd("PAGEEXISTS($datatable.pid) = 1");
50                $subOr->whereAnd('(ASSIGNED = 1 OR ASSIGNED IS NULL)');
51
52                // add conditional schema assignment check
53                $this->qb->addLeftJoin(
54                    $datatable,
55                    'schema_assignments',
56                    '',
57                    "$datatable.pid != ''
58                    AND $datatable.pid = schema_assignments.pid
59                    AND schema_assignments.tbl = '{$schema->getTable()}'"
60                );
61
62                $this->qb->addSelectColumn($datatable, 'rid');
63                $this->qb->addSelectColumn($datatable, 'pid', 'PID');
64                $this->qb->addSelectColumn($datatable, 'rev');
65                $this->qb->addSelectColumn('schema_assignments', 'assigned', 'ASSIGNED');
66                $this->qb->addGroupByColumn($datatable, 'pid');
67                $this->qb->addGroupByColumn($datatable, 'rid');
68
69                $first_table = $datatable;
70            }
71            $this->qb->filters()->whereAnd($this->addPublishClauses($datatable));
72        }
73    }
74
75    /**
76     * Add the columns to select, handling multis
77     *
78     * @param Column[] $columns
79     */
80    public function addColumns($columns)
81    {
82        $sep = Search::CONCAT_SEPARATOR;
83        $n = 0;
84        foreach ($columns as $col) {
85            $CN = 'C' . $n++;
86
87            if ($col->isMulti()) {
88                $datatable = "data_{$col->getTable()}";
89                $multitable = "multi_{$col->getTable()}";
90                $MN = $this->qb->generateTableAlias('M');
91
92                $this->qb->addLeftJoin(
93                    $datatable,
94                    $multitable,
95                    $MN,
96                    "$datatable.pid = $MN.pid AND $datatable.rid = $MN.rid AND
97                     $datatable.rev = $MN.rev AND
98                     $MN.colref = {$col->getColref()}"
99                );
100
101                $col->getType()->select($this->qb, $MN, 'value', $CN);
102                $sel = $this->qb->getSelectStatement($CN);
103                $this->qb->addSelectStatement("GROUP_CONCAT_DISTINCT($sel, '$sep')", $CN);
104            } else {
105                $col->getType()->select($this->qb, 'data_' . $col->getTable(), $col->getColName(), $CN);
106                $this->qb->addGroupByStatement($CN);
107            }
108        }
109    }
110
111    /**
112     * Add the filters to the query
113     *
114     * All given filters are added to their own subclause.
115     *
116     * @param array[] $filters
117     */
118    public function addFilters($filters)
119    {
120        if (!$filters) return; // ignore empty filters
121
122        $subClause = $this->qb->filters()->where('AND');
123
124        foreach ($filters as $filter) {
125            /** @var Column $col */
126            list($col, $value, $comp, $op) = $filter;
127
128            $datatable = "data_{$col->getTable()}";
129            $multitable = "multi_{$col->getTable()}";
130
131            /** @var $col Column */
132            if ($col->isMulti()) {
133                $MN = $this->qb->generateTableAlias('MN');
134
135                $this->qb->addLeftJoin(
136                    $datatable,
137                    $multitable,
138                    $MN,
139                    "$datatable.pid = $MN.pid AND $datatable.rid = $MN.rid AND
140                     $datatable.rev = $MN.rev AND
141                     $MN.colref = {$col->getColref()}"
142                );
143                $coltbl = $MN;
144                $colnam = 'value';
145            } else {
146                $coltbl = $datatable;
147                $colnam = $col->getColName();
148            }
149
150            $col->getType()->filter($subClause, $coltbl, $colnam, $comp, $value, $op); // type based filter
151        }
152    }
153
154    /**
155     * Add the sort by clauses to the query
156     *
157     * We always sort by the single val column which contains a copy of the first value of the multi column
158     *
159     * @param array[] $sorts
160     */
161    public function addSorts($sorts)
162    {
163        foreach ($sorts as $sort) {
164            list($col, $asc, $nc) = $sort;
165            /** @var $col Column */
166            $colname = $col->getColName(false);
167            if ($nc) $colname .= ' COLLATE NOCASE';
168            $col->getType()->sort($this->qb, 'data_' . $col->getTable(), $colname, $asc ? 'ASC' : 'DESC');
169        }
170    }
171
172    /**
173     * @param string $datatable
174     * @return string
175     */
176    public function addPublishClauses($datatable)
177    {
178        $latestClause = "IS_PUBLISHER($datatable.pid)";
179        if ($this->selectLatest) {
180            $latestClause .= " AND $datatable.latest = 1";
181        }
182        $publishedClause = "IS_PUBLISHER($datatable.pid) !=1 AND $datatable.published = 1";
183
184        return "( ($latestClause) OR ($publishedClause) )";
185    }
186
187    /**
188     * Access to the underlying QueryBuilder
189     *
190     * @return QueryBuilder
191     */
192    public function getQueryBuilder()
193    {
194        return $this->qb;
195    }
196
197    /**
198     * Get the SQL query and parameters
199     *
200     * Shortcut for $this->getQueryBuilder()->getSQL()
201     *
202     * @return array ($sql, $params)
203     */
204    public function getSQL()
205    {
206        return $this->qb->getSQL();
207    }
208
209    /**
210     * Allows disabling default 'latest = 1' clause in select statement
211     *
212     * @param bool $selectLatest
213     */
214    public function setSelectLatest(bool $selectLatest)
215    {
216        $this->selectLatest = $selectLatest;
217    }
218}
219