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 const 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 * Transform the set search parameters into a statement 108 * 109 * @todo limit to the newest data! 110 * @return string 111 */ 112 public function getSQL() { 113 if(!$this->columns) throw new StructException('nocolname'); 114 115 $from = ''; 116 $select = ''; 117 $order = ''; 118 $grouping = array(); 119 $opts = array(); 120 $where = '1 = 1'; 121 122 // basic tables 123 $first = ''; 124 foreach($this->schemas as $schema) { 125 if($first) { 126 // follow up tables 127 $from .= "\nLEFT OUTER JOIN data_{$schema->getTable()} ON data_$first.pid = data_{$schema->getTable()}.pid"; 128 } else { 129 // first table 130 $select .= "data_{$schema->getTable()}.pid as PID, "; 131 $from .= "data_{$schema->getTable()}"; 132 $first = $schema->getTable(); 133 } 134 135 $where .= "\nAND data_{$schema->getTable()}.latest = 1"; 136 } 137 138 // columns to select, handling multis 139 $sep = self::CONCAT_SEPARATOR; 140 $n = 0; 141 foreach($this->columns as $col) { 142 $CN = 'C' . $n++; 143 144 if($col->isMulti()) { 145 $tn = 'M' . $col->getColref(); 146 $select .= "GROUP_CONCAT($tn.value, '$sep') AS $CN, "; 147 $from .= "\nLEFT OUTER JOIN multivals AS $tn"; 148 $from .= " ON data_{$col->getTable()}.pid = $tn.pid AND data_{$col->getTable()}.rev = $tn.rev"; 149 $from .= " AND $tn.tbl = '{$col->getTable()}' AND $tn.colref = {$col->getColref()}\n"; 150 } else { 151 $select .= 'data_' . $col->getTable() . ' . col' . $col->getColref() . " AS $CN, "; 152 $grouping[] = $CN; 153 } 154 } 155 $select = rtrim($select, ', '); 156 157 // where clauses 158 foreach($this->filter as $filter) { 159 list($col, $value, $comp, $type) = $filter; 160 161 /** @var $col Column */ 162 if($col->isMulti()) { 163 $tn = 'MN' . $col->getColref(); // FIXME this joins a second time if the column was selected before 164 $from .= "\nLEFT OUTER JOIN multivals AS $tn"; 165 $from .= " ON data_{$col->getTable()}.pid = $tn.pid AND data_{$col->getTable()}.rev = $tn.rev"; 166 $from .= " AND $tn.tbl = '{$col->getTable()}' AND $tn.colref = {$col->getColref()}\n"; 167 168 $column = "$tn.value"; 169 } else { 170 $column = "data_{$col->getTable()}.col{$col->getColref()}"; 171 } 172 173 list($wsql, $wopt) = $col->getType()->compare($column, $comp, $value); 174 $opts = array_merge($opts, $wopt); 175 176 $where .= " $type $wsql"; 177 } 178 179 // sorting 180 foreach($this->sortby as $sort) { 181 list($col, $asc) = $sort; 182 183 /** @var $col Column */ 184 if($col->isMulti()) { 185 // FIXME how to sort by multival? 186 // FIXME what if sort by non merged multival? 187 } else { 188 $order .= "data_{$col->getTable()}.col{$col->getColref()} "; 189 $order .= ($asc) ? 'ASC' : 'DESC'; 190 $order .= ', '; 191 } 192 } 193 $order = rtrim($order, ', '); 194 195 $sql = "SELECT $select\n FROM $from\nWHERE $where\nGROUP BY " . join(', ', $grouping); 196 if($order) $sql .= "\nORDER BY $order"; 197 198 {#debugging 199 $res = $this->sqlite->query($sql, $opts); 200 $data = $this->sqlite->res2arr($res); 201 $this->sqlite->res_close($res); 202 print_r($data); 203 } 204 205 return $sql; 206 } 207 208 /** 209 * Find a column to be used in the search 210 * 211 * @param string $colname may contain an alias 212 * @return bool|Column 213 */ 214 protected function findColumn($colname) { 215 if(!$this->schemas) throw new StructException('noschemas'); 216 217 // resolve the alias or table name 218 list($table, $colname) = explode('.', $colname, 2); 219 if(!$colname) { 220 $colname = $table; 221 $table = ''; 222 } 223 if($table && isset($this->aliases[$table])) { 224 $table = $this->aliases[$table]; 225 } 226 227 if(!$colname) throw new StructException('nocolname'); 228 229 // if table name given search only that, otherwiese try all for matching column name 230 if($table) { 231 $schemas = array($table => $this->schemas[$table]); 232 } else { 233 $schemas = $this->schemas; 234 } 235 236 // find it 237 $col = false; 238 foreach($schemas as $schema) { 239 $col = $schema->findColumn($colname); 240 if($col) break; 241 } 242 243 return $col; 244 } 245 246} 247 248 249