-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdarkroast.php
272 lines (213 loc) · 7.93 KB
/
darkroast.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
<?php
namespace DarkRoast;
require_once('IDataProvider.php');
interface IDarkRoast {
/**
* Execute the build query and returns the result as an array.
* When the query contains placeholders, invoke this function with unbound arguments. Each placeholder
* N is replaced by the Nth argument.
* @param mixed ...$bindings Bound values for placeholders.
* @return array Query output table.
*/
public function execute(...$bindings);
}
interface IReorderable {
public function parenthesis();
}
interface ITerminalFieldExpression {
public function alias();
public function name($alias);
public function sortAscending();
public function sortDescending();
}
interface IEqualityFilterExpression {
public function equals($operand);
public function lessThan($operand);
public function greaterThan($operand);
public function lessOrEqualThan($operand);
public function greaterOrEqualThan($operand);
}
interface IFieldExpression extends ITerminalFieldExpression, IReorderable, IEqualityFilterExpression {
public function add($operand);
public function minus($operand);
public function multiply($operand);
public function divide($operand);
public function isDefined();
public function isUndefined();
}
interface IAggregateableExpression extends IFieldExpression {
public function sum();
public function max();
public function min();
public function count();
public function countUnique();
public function group();
}
interface IBuilder {
public function build($selectors, $filter = null, $groupFilter = null, $window = [0, null]);
}
interface IFilter extends IReorderable {
public function _and($condition);
public function _or($condition);
public function not($condition);
public function exists();
}
class Query {
/**
* Add selectors to existing selectors of query.
* @param ITerminalFieldExpression ...$selectors Selectors to add.
* @return $this
*/
public function select(ITerminalFieldExpression ...$selectors) {
$this->selectors = array_merge($this->selectors, $selectors);
return $this;
}
/**
* Construct and overwrite the query filter by concatenating the `$conditions` using the logical and operator.
* The filter is evaluated for of all rows before aggregation has taken place. It may therefore only depend on non-aggregate
* field expressions.
* @param IFilter ...$conditions
* @return $this
*/
public function filter(IFilter ...$conditions) {
$this->filter = null;
$this->_and(...$conditions);
return $this;
}
/**
* Construct and overwrite the query group filter by concatenating the `$conditions` using the logical and operator.
* The group filter is evaluated for all aggregate rows, after aggregation has taken place. It may not depend on non-aggregate
* field expressions.
* @param IFilter ...$conditions
* @return $this
*/
public function groupFilter(IFilter ...$conditions) {
$this->groupFilter = null;
$this->addConditions($conditions, "_and", $this->groupFilter);
return $this;
}
public function _or(IFilter ...$conditions) {
return $this->addConditions($conditions, "_or", $this->filter);
}
public function _and(IFilter ...$conditions) {
return $this->addConditions($conditions, "_and", $this->filter);
}
public function build(IBuilder $builder) {
return $builder->build($this->selectors, $this->filter, $this->groupFilter, $this->window);
}
public function table($provider) {
$userFieldNum = 0;
$fieldAliases = [];
foreach ($this->selectors as &$selector) {
$userFieldNum++;
$alias = $selector->alias();
if ($alias === '') {
$alias = "UserField{$userFieldNum}";
$selector = $selector->name($alias);
}
$fieldAliases[] = $alias;
}
return $provider->createTable($fieldAliases, $this);
}
/**
* Shorthand to build and execute query.
* @param $builder IBuilder
* @param ...$params mixed Unbound arguments forwarded to IDarkRoast::execute
* @see Query::build
* @see IDarkRoast::execute
* @return array Query output table.
*/
public function execute(IBuilder $builder, ...$params) {
$darkRoast = $this->build($builder);
return $darkRoast->execute(...$params);
}
/**
* Limit the result of the query to at most rows [`$offset`, `$offset + $limit - 1`].
* If `$limit` is null, all rows including and after `$offset` are returned when executing the query.
* @param int $offset Must be positive.
* @param int|null $limit Must be positive or null.
* @return $this
*/
public function window($offset, $limit = null) {
if (!is_int($offset)) throw new \InvalidArgumentException('Offset must be an integer.');
if ($offset < 0) throw new \DomainException('Offset must be positive');
if (isset($limit)) {
if (!is_int($limit)) throw new \InvalidArgumentException('Limit must be an integer.');
if ($limit < 0) throw new \DomainException('Offset must be positive');
}
$this->window = [$offset, $limit];
return $this;
}
private function addConditions(array $conditions, $logicalOperator, &$targetFilter) {
$firstCondition = isset($targetFilter) ? $targetFilter : p(array_shift($conditions));
$targetFilter = array_reduce($conditions, function($filter, $condition) use($logicalOperator) {
return $filter->$logicalOperator(p($condition));
}, $firstCondition);
return $this;
}
private $selectors = [];
private $filter = null;
private $groupFilter = null;
private $window = [0, null];
}
/**
* Construct a new query with the specified selectors.
* @param ITerminalFieldExpression ...$selectors
* @return Query
*/
function select(ITerminalFieldExpression ...$selectors) {
$query = new Query();
return $query->select(...$selectors);
}
/**
* Constructs a table expression from a query.
* The fields of the resulting table expression are defined by the selectors of the source query, and are applicable everywhere
* where regular fields can be used.
* If the x*th* selector is unnamed and no name can be inferred, the corresponding field in the table expression is named `UserField{x}`.
* Note that execution of the table expression is deferred and tied to execution of the outer query.
* For DBMS providers, table expressions are typically implemented as (correlated) sub-queries.
* @param Query $query
* @param IDataProvider $provider
* @return mixed
*/
function table(Query $query, IDataProvider $provider) {
return $query->table($provider);
}
function reduceFields(callable $func, ITerminalFieldExpression ...$fields) {
if (count($fields) < 2) throw new \InvalidArgumentException('Operation requires at least two operands.');
$firstField = array_shift($fields);
return array_reduce($fields, $func, $firstField);
}
function sum(ITerminalFieldExpression ...$fields) {
return reduceFields(function($carry, $fieldExpression) {
return $carry->add($fieldExpression);
}, ...$fields);
}
/**
* Constructs an exists filter from one or multiple filter expressions which are concatenated using the logical `and` operator.
* The resulting filter expressions is evaluated in the context of an anonymous table expression for each row of the enclosing query.
* Any row of the enclosing query for which the table expression returns nothing output is filtered out.
* The exists filter is most useful when the supplied conditions correlate with the outer query.
* @example "demo/basic.php" Exist filter in action.
*
* @param IFilter ...$conditions
* @return mixed
*/
function exists(IFilter ...$conditions) {
$firstCondition = array_shift($conditions);
$condition = array_reduce($conditions, function($filter, $condition) {
return $filter->_and($condition);
}, $firstCondition);
return $condition->exists();
}
function coalesce(array $array, $key, $default = null) {
return isset($array[$key]) ? $array[$key] : $default;
}
/**
* Group parts of a field or filter expression to change order of operations.
* @param IReorderable $queryElement
* @return mixed
*/
function p(IReorderable $queryElement) {
return $queryElement->parenthesis();
}