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