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 [$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 [$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