xref: /plugin/struct/meta/Search.php (revision 5511bd5b07a4ce4f72c790c57a0579633941affd)
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