1<?php 2 3namespace plugin\struct\meta; 4 5class Search { 6 /** 7 * This separator will be used to concat multi values to flatten them in the result set 8 */ 9 const CONCAT_SEPARATOR = "\n!_-_-_-_-_!\n"; 10 11 /** 12 * The list of known and allowed comparators 13 */ 14 static public $COMPARATORS = array( 15 '<', '>', '<=', '>=', '!=', '!~', '~' 16 ); 17 18 /** @var \helper_plugin_sqlite */ 19 protected $sqlite; 20 21 /** @var Schema[] list of schemas to query */ 22 protected $schemas = array(); 23 24 /** @var Column[] list of columns to select */ 25 protected $columns = array(); 26 27 /** @var array the sorting of the result */ 28 protected $sortby = array(); 29 30 /** @var array the filters */ 31 protected $filter = array(); 32 33 /** @var array list of aliases tables can be referenced by */ 34 protected $aliases = array(); 35 36 /** 37 * Search constructor. 38 */ 39 public function __construct() { 40 /** @var \helper_plugin_struct_db $plugin */ 41 $plugin = plugin_load('helper', 'struct_db'); 42 $this->sqlite = $plugin->getDB(); 43 } 44 45 /** 46 * Add a schema to be searched 47 * 48 * Call multiple times for multiple schemas. 49 * 50 * @param string $table 51 * @param string $alias 52 */ 53 public function addSchema($table, $alias = '') { 54 $this->schemas[$table] = new Schema($table); 55 if($alias) $this->aliases[$alias] = $table; 56 } 57 58 /** 59 * Add a column to be returned by the search 60 * 61 * Call multiple times for multiple columns. Be sure the referenced tables have been 62 * added before 63 * 64 * @param string $colname may contain an alias 65 */ 66 public function addColumn($colname) { 67 $col = $this->findColumn($colname); 68 if(!$col) return; //FIXME do we really want to ignore missing columns? 69 $this->columns[] = $col; 70 } 71 72 /** 73 * Add sorting options 74 * 75 * Call multiple times for multiple columns. Be sure the referenced tables have been 76 * added before 77 * 78 * @param string $colname may contain an alias 79 * @param bool $asc sort direction (ASC = true, DESC = false) 80 */ 81 public function addSort($colname, $asc = true) { 82 $col = $this->findColumn($colname); 83 if(!$col) return; //FIXME do we really want to ignore missing columns? 84 85 $this->sortby[] = array($col, $asc); 86 } 87 88 /** 89 * Adds a filter 90 * 91 * @param string $colname may contain an alias 92 * @param string $value 93 * @param string $comp @see self::COMPARATORS 94 * @param string $type either 'OR' or 'AND' 95 */ 96 public function addFilter($colname, $value, $comp, $type = 'OR') { 97 if(!in_array($comp, self::$COMPARATORS)) throw new StructException("Bad comperator. Use " . join(',', self::$COMPARATORS)); 98 if($type != 'OR' && $type != 'AND') throw new StructException('Bad filter type . Only AND or OR allowed'); 99 100 $col = $this->findColumn($colname); 101 if(!$col) return; //FIXME do we really want to ignore missing columns? 102 103 $this->filter[] = array($col, $value, $comp, $type); 104 } 105 106 /** 107 * Execute this search and return the result 108 * 109 * The result is a two dimensional array of array. Each cell contains an array with 110 * the keys 'col' (containing a Column object) and 'val' containing the value(s) 111 */ 112 public function execute() { 113 list($sql, $opts) = $this->getSQL(); 114 115 $res = $this->sqlite->query($sql, $opts); 116 $data = $this->sqlite->res2arr($res); 117 $this->sqlite->res_close($res); 118 119 $result = array(); 120 foreach($data as $row) { 121 $C = 0; 122 $resrow = array(); 123 foreach($this->columns as $col) { 124 $rescol = array(); 125 $rescol['col'] = $col; 126 $rescol['val'] = $row["C$C"]; 127 if($col->isMulti()) { 128 $rescol['val'] = explode(self::CONCAT_SEPARATOR,$rescol['val']); 129 } 130 $resrow[] = $rescol; 131 $C++; 132 } 133 $result[] = $resrow; 134 } 135 return $result; 136 } 137 138 /** 139 * Transform the set search parameters into a statement 140 * 141 * @return array ($sql, $opts) The SQL and parameters to execute 142 */ 143 public function getSQL() { 144 if(!$this->columns) throw new StructException('nocolname'); 145 146 $from = ''; 147 $select = ''; 148 $order = ''; 149 $grouping = array(); 150 $opts = array(); 151 $where = '1 = 1'; 152 153 // basic tables 154 $first = ''; 155 foreach($this->schemas as $schema) { 156 if($first) { 157 // follow up tables 158 $from .= "\nLEFT OUTER JOIN data_{$schema->getTable()} ON data_$first.pid = data_{$schema->getTable()}.pid"; 159 } else { 160 // first table 161 $select .= "data_{$schema->getTable()}.pid as PID, "; 162 $from .= "data_{$schema->getTable()}"; 163 $first = $schema->getTable(); 164 } 165 166 $where .= "\nAND data_{$schema->getTable()}.latest = 1"; 167 } 168 169 // columns to select, handling multis 170 $sep = self::CONCAT_SEPARATOR; 171 $n = 0; 172 foreach($this->columns as $col) { 173 $CN = 'C' . $n++; 174 175 if($col->isMulti()) { 176 $tn = 'M' . $col->getColref(); 177 $select .= "GROUP_CONCAT($tn.value, '$sep') AS $CN, "; 178 $from .= "\nLEFT OUTER JOIN multi_{$col->getTable()} AS $tn"; 179 $from .= " ON data_{$col->getTable()}.pid = $tn.pid AND data_{$col->getTable()}.rev = $tn.rev"; 180 $from .= " AND $tn.colref = {$col->getColref()}\n"; 181 } else { 182 $select .= 'data_' . $col->getTable() . '.col' . $col->getColref() . " AS $CN, "; 183 $grouping[] = $CN; 184 } 185 } 186 $select = rtrim($select, ', '); 187 188 // where clauses 189 foreach($this->filter as $filter) { 190 list($col, $value, $comp, $type) = $filter; 191 192 /** @var $col Column */ 193 if($col->isMulti()) { 194 $tn = 'MN' . $col->getColref(); // FIXME this joins a second time if the column was selected before 195 $from .= "\nLEFT OUTER JOIN multi_{$col->getTable()} AS $tn"; 196 $from .= " ON data_{$col->getTable()}.pid = $tn.pid AND data_{$col->getTable()}.rev = $tn.rev"; 197 $from .= " AND $tn.colref = {$col->getColref()}\n"; 198 199 $column = "$tn.value"; 200 } else { 201 $column = "data_{$col->getTable()}.col{$col->getColref()}"; 202 } 203 204 list($wsql, $wopt) = $col->getType()->compare($column, $comp, $value); 205 $opts = array_merge($opts, $wopt); 206 207 $where .= " $type $wsql"; 208 } 209 210 // sorting 211 foreach($this->sortby as $sort) { 212 list($col, $asc) = $sort; 213 214 /** @var $col Column */ 215 if($col->isMulti()) { 216 // FIXME how to sort by multival? 217 // FIXME what if sort by non merged multival? 218 } else { 219 $order .= "data_{$col->getTable()}.col{$col->getColref()} "; 220 $order .= ($asc) ? 'ASC' : 'DESC'; 221 $order .= ', '; 222 } 223 } 224 $order = rtrim($order, ', '); 225 226 $sql = "SELECT $select\n FROM $from\nWHERE $where\nGROUP BY " . join(', ', $grouping); 227 if($order) $sql .= "\nORDER BY $order"; 228 229 return array($sql, $opts); 230 } 231 232 /** 233 * Find a column to be used in the search 234 * 235 * @param string $colname may contain an alias 236 * @return bool|Column 237 */ 238 protected function findColumn($colname) { 239 if(!$this->schemas) throw new StructException('noschemas'); 240 241 // resolve the alias or table name 242 list($table, $colname) = explode('.', $colname, 2); 243 if(!$colname) { 244 $colname = $table; 245 $table = ''; 246 } 247 if($table && isset($this->aliases[$table])) { 248 $table = $this->aliases[$table]; 249 } 250 251 if(!$colname) throw new StructException('nocolname'); 252 253 // if table name given search only that, otherwiese try all for matching column name 254 if($table) { 255 $schemas = array($table => $this->schemas[$table]); 256 } else { 257 $schemas = $this->schemas; 258 } 259 260 // find it 261 $col = false; 262 foreach($schemas as $schema) { 263 $col = $schema->findColumn($colname); 264 if($col) break; 265 } 266 267 return $col; 268 } 269 270} 271 272 273