Completed
Push — master ( 4d8127...9cd25a )
by Chris
02:49
created

AbstractSqlTranslator::prepareGroupBy()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
namespace Darya\Database\Query;
3
4
use InvalidArgumentException;
5
use Darya\Database;
6
use Darya\Database\Query\Translator;
7
use Darya\Database\Storage\Query\Join;
8
use Darya\Storage;
9
10
/**
11
 * An abstract query translator that prepares SQL common across more than one
12
 * RDBMS.
13
 * 
14
 * @author Chris Andrew <[email protected]>
15
 */
16
abstract class AbstractSqlTranslator implements Translator
17
{
18
	/**
19
	 * Filter comparison operators.
20
	 * 
21
	 * @var array
22
	 */
23
	protected $operators = array('>=', '<=', '>', '<', '=', '!=', '<>', 'in', 'not in', 'is', 'is not', 'like', 'not like');
24
	
25
	/**
26
	 * Placeholder for values in prepared queries.
27
	 * 
28
	 * @var string
29
	 */
30
	protected $placeholder = '?';
31
	
32
	/**
33
	 * Concatenates the given set of strings that aren't empty.
34
	 * 
35
	 * Runs implode() after filtering out empty elements.
36
	 * 
37
	 * Delimiter defaults to a single whitespace character.
38
	 * 
39
	 * @param array  $strings
40
	 * @param string $delimiter [optional]
41
	 * @return string
42
	 */
43
	protected static function concatenate($strings, $delimiter = ' ')
44
	{
45
		$strings = array_filter($strings, function ($value) {
46
			return !empty($value);
47
		});
48
		
49
		return implode($delimiter, $strings);
50
	}
51
	
52
	/**
53
	 * Determine whether the given limit and offset will make a difference to
54
	 * a statement.
55
	 * 
56
	 * Simply determines whether both are non-zero integers.
57
	 * 
58
	 * @param int $limit
59
	 * @param int $offset
60
	 * @return bool
61
	 */
62
	protected static function limitIsUseful($limit, $offset)
63
	{
64
		return (int) $limit !== 0 || (int) $offset !== 0;
65
	}
66
	
67
	/**
68
	 * Translate the given storage query into an SQL query.
69
	 * 
70
	 * @param Storage\Query $storageQuery
71
	 * @return Database\Query
72
	 * @throws InvalidArgumentException
73
	 */
74
	public function translate(Storage\Query $storageQuery)
75
	{
76
		$type = $storageQuery->type;
77
		
78
		$method = 'translate' . ucfirst($type);
79
		
80
		if (!method_exists($this, $method)) {
81
			throw new InvalidArgumentException("Could not translate query of unknown type '$type'");
82
		}
83
		
84
		$query = call_user_func_array(array($this, $method), array($storageQuery));
85
		
86
		return $query;
87
	}
88
	
89
	/**
90
	 * Translate a query that creates a record.
91
	 * 
92
	 * @param Storage\Query $storageQuery
93
	 * @return Database\Query
94
	 */
95
	protected function translateCreate(Storage\Query $storageQuery)
96
	{
97
		if ($storageQuery instanceof Database\Storage\Query && $storageQuery->insertSubquery) {
98
			return new Database\Query(
99
				$this->prepareInsertSelect($storageQuery->resource, $storageQuery->fields, $storageQuery->insertSubquery),
0 ignored issues
show
Documentation introduced by
The property $resource is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Documentation introduced by
The property $fields is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
100
				$this->parameters($storageQuery->insertSubquery)
101
			);
102
		}
103
		
104
		return new Database\Query(
105
			$this->prepareInsert($storageQuery->resource, $storageQuery->data),
106
			$this->parameters($storageQuery)
107
		);
108
	}
109
	
110
	/**
111
	 * Translate a query that reads records.
112
	 * 
113
	 * @param Storage\Query $storageQuery
114
	 * @return Database\Query
115
	 */
116
	protected function translateRead(Storage\Query $storageQuery)
117
	{
118
		if ($storageQuery instanceof Database\Storage\Query) {
119
			return $this->translateDatabaseRead($storageQuery);
120
		}
121
		
122
		return new Database\Query(
123
			$this->prepareSelect(
124
				$storageQuery->resource,
125
				$this->prepareColumns($storageQuery->fields),
126
				null,
127
				$this->prepareWhere($storageQuery->filter),
128
				$this->prepareOrderBy($storageQuery->order),
129
				$this->prepareLimit($storageQuery->limit, $storageQuery->offset),
130
				null,
131
				null,
132
				$storageQuery->distinct
133
			),
134
			$this->parameters($storageQuery)
135
		);
136
	}
137
	
138
	/**
139
	 * Translate a database storage query that reads records.
140
	 * 
141
	 * @param Database\Storage\Query $storageQuery
142
	 */
143
	protected function translateDatabaseRead(Database\Storage\Query $storageQuery)
144
	{
145
		return new Database\Query(
146
			$this->prepareSelect($storageQuery->resource,
0 ignored issues
show
Documentation introduced by
The property $resource is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
147
				$this->prepareColumns($storageQuery->fields),
0 ignored issues
show
Documentation introduced by
The property $fields is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
148
				$this->prepareJoins($storageQuery->joins),
149
				$this->prepareWhere($storageQuery->filter),
0 ignored issues
show
Documentation introduced by
The property $filter is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
150
				$this->prepareOrderBy($storageQuery->order),
0 ignored issues
show
Documentation introduced by
The property $order is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
151
				$this->prepareLimit($storageQuery->limit, $storageQuery->offset),
0 ignored issues
show
Documentation introduced by
The property $limit is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Documentation introduced by
The property $offset is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
152
				$this->prepareGroupBy($storageQuery->groupings),
153
				$this->prepareHaving($storageQuery->having),
154
				$storageQuery->distinct
0 ignored issues
show
Documentation introduced by
The property $distinct is declared protected in Darya\Storage\Query. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
155
			),
156
			$this->parameters($storageQuery)
157
		);
158
	}
159
	
160
	/**
161
	 * Translate a query that updates records.
162
	 * 
163
	 * @param Storage\Query $storageQuery
164
	 * @return Database\Query
165
	 */
166
	protected function translateUpdate(Storage\Query $storageQuery)
167
	{
168
		return new Database\Query(
169
			$this->prepareUpdate($storageQuery->resource, $storageQuery->data,
170
				$this->prepareWhere($storageQuery->filter),
171
				$this->prepareLimit($storageQuery->limit, $storageQuery->offset)
172
			),
173
			$this->parameters($storageQuery)
174
		);
175
	}
176
	
177
	/**
178
	 * Translate a query that deletes records.
179
	 * 
180
	 * @param Storage\Query $storageQuery
181
	 * @return Database\Query
182
	 */
183
	protected function translateDelete(Storage\Query $storageQuery)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
184
	{
185
		return new Database\Query(
186
			$this->prepareDelete($storageQuery->resource,
187
				$this->prepareWhere($storageQuery->filter),
188
				$this->prepareLimit($storageQuery->limit, $storageQuery->offset)
189
			),
190
			$this->parameters($storageQuery)
191
		);
192
	}
193
	
194
	/**
195
	 * Resolve the given value as an identifier.
196
	 * 
197
	 * @param mixed $identifier
198
	 * @return string
199
	 */
200
	abstract protected function resolveIdentifier($identifier);
201
	
202
	/**
203
	 * Prepare the given identifier.
204
	 * 
205
	 * If the value is translatable, it is translated.
206
	 * 
207
	 * If the value is an array, it is recursively prepared.
208
	 * 
209
	 * @param mixed $identifier
210
	 * @return mixed
211
	 */
212
	protected function identifier($identifier)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
213
	{
214
		if (is_array($identifier)) {
215
			return array_map(array($this, 'identifier'), $identifier);
216
		}
217
		
218
		if ($this->translatable($identifier)) {
219
			return $this->translateValue($identifier);
220
		}
221
		
222
		return $this->resolveIdentifier($identifier);
223
	}
224
	
225
	/**
226
	 * Determine whether the given value is translatable.
227
	 * 
228
	 * @param mixed $value
229
	 * @return bool
230
	 */
231
	protected function translatable($value)
232
	{
233
		return $value instanceof Storage\Query\Builder || $value instanceof Storage\Query;
234
	}
235
	
236
	/**
237
	 * Translate the given translatable query.
238
	 * 
239
	 * Helper for handling the translation of query objects from query builders.
240
	 * 
241
	 * @param mixed $query
242
	 * @return Database\Query
243
	 */
244
	protected function translateTranslatable($query)
245
	{
246
		if (!$this->translatable($query)) {
247
			throw new InvalidArgumentException("Cannot translate query of type '" . get_class($query) . "'");
248
		}
249
		
250
		if ($query instanceof Storage\Query\Builder) {
251
			$query = $query->query;
252
		}
253
		
254
		if ($query instanceof Storage\Query) {
255
			return $this->translate($query);
256
		}
257
	}
258
	
259
	/**
260
	 * Translate the given value if it is a query or query builder.
261
	 * 
262
	 * Returns the argument as is otherwise.
263
	 * 
264
	 * @param mixed $value
265
	 * @return string
266
	 */
267
	protected function translateValue($value)
268
	{
269
		$query = $this->translateTranslatable($value);
270
		
271
		return "($query)";
272
	}
273
	
274
	/**
275
	 * Prepare the given value for a prepared query.
276
	 * 
277
	 * If the value translatable, it is translated.
278
	 * 
279
	 * If the value is an array, it is recursively prepared.
280
	 * 
281
	 * @param mixed $value
282
	 * @return array|string
283
	 */
284
	protected function value($value)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
285
	{
286
		if (is_array($value)) {
287
			return array_map(array($this, 'value'), $value);
288
		}
289
		
290
		if ($this->translatable($value)) {
291
			return $this->translateValue($value);
292
		}
293
		
294
		return $this->resolveValue($value);
295
	}
296
	
297
	/**
298
	 * Resolve a placeholder or constant for the given parameter value.
299
	 * 
300
	 * @param mixed $value
301
	 * @return string
302
	 */
303
	protected function resolveValue($value)
304
	{
305
		if ($value === null) {
306
			return 'NULL';
307
		}
308
		
309
		if (is_bool($value)) {
310
			return $value ? 'TRUE' : 'FALSE';
311
		}
312
		
313
		return $this->placeholder;
314
	}
315
	
316
	/**
317
	 * Determine whether the given value resolves a placeholder.
318
	 * 
319
	 * @param mixed $value
320
	 * @return bool
321
	 */
322
	protected function resolvesPlaceholder($value)
323
	{
324
		return $this->resolveValue($value) === $this->placeholder;
325
	}
326
	
327
	/**
328
	 * Prepare a set of column aliases.
329
	 * 
330
	 * Uses the keys of the given array as identifiers and appends them to their
331
	 * values.
332
	 * 
333
	 * @param array $columns
334
	 * @return array
335
	 */
336
	protected function prepareColumnAliases(array $columns)
337
	{
338
		foreach ($columns as $alias => &$column) {
339
			if (is_string($alias) && preg_match('/^[\w]/', $alias)) {
340
				$aliasIdentifier = $this->identifier($alias);
341
				$column = "$column $aliasIdentifier";
342
			}
343
		}
344
		
345
		return $columns;
346
	}
347
	
348
	/**
349
	 * Prepare the given columns as a string.
350
	 * 
351
	 * @param array|string $columns
352
	 * @return string
353
	 */
354
	protected function prepareColumns($columns)
355
	{
356
		if (empty($columns)) {
357
			return '*';
358
		}
359
		
360
		$columns = (array) $this->identifier($columns);
361
		
362
		$columns = $this->prepareColumnAliases($columns);
363
		
364
		return implode(', ', $columns);
365
	}
366
	
367
	/**
368
	 * Determine whether the given operator is valid.
369
	 * 
370
	 * @param string $operator
371
	 * @return bool
372
	 */
373
	protected function validOperator($operator)
374
	{
375
		$operator = trim($operator);
376
		
377
		return in_array(strtolower($operator), $this->operators);
378
	}
379
	
380
	/**
381
	 * Prepare the given conditional operator.
382
	 * 
383
	 * Returns the equals operator if given value is not in the set of valid
384
	 * operators.
385
	 * 
386
	 * @param string $operator
387
	 * @return string
388
	 */
389
	protected function prepareRawOperator($operator)
390
	{
391
		$operator = trim($operator);
392
		
393
		return $this->validOperator($operator) ? strtoupper($operator) : '=';
394
	}
395
	
396
	/**
397
	 * Prepare an appropriate conditional operator for the given value.
398
	 * 
399
	 * @param string $operator
400
	 * @param mixed  $value    [optional]
401
	 * @return string
402
	 */
403
	protected function prepareOperator($operator, $value = null)
404
	{
405
		$operator = $this->prepareRawOperator($operator);
406
		
407 View Code Duplication
		if (!$this->resolvesPlaceholder($value)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
408
			if ($operator === '=') {
409
				$operator = 'IS';
410
			}
411
			
412
			if ($operator === '!=') {
413
				$operator = 'IS NOT';
414
			}
415
		}
416
		
417
		if (is_array($value)) {
418
			if ($operator === '=') {
419
				$operator = 'IN';
420
			}
421
		
422
			if ($operator === '!=') {
423
				$operator = 'NOT IN';
424
			}
425
		}
426
		
427
		return $operator;
428
	}
429
	
430
	/**
431
	 * Prepare a join type.
432
	 * 
433
	 * @param string $type
434
	 * @return string
435
	 */
436
	protected function prepareJoinType($type)
437
	{
438
		if (in_array($type, array('left', 'right'))) {
439
			return strtoupper($type) . ' JOIN';
440
		}
441
		
442
		return 'JOIN';
443
	}
444
	
445
	/**
446
	 * Prepare a join table.
447
	 * 
448
	 * @param Join $join
449
	 * @return string
450
	 */
451
	protected function prepareJoinTable(Join $join)
452
	{
453
		$table = $this->identifier($join->resource);
454
		$alias = $this->identifier($join->alias);
455
		
456
		return $alias ? "$table $alias" : $table;
457
	}
458
	
459
	/**
460
	 * Prepare a single join condition.
461
	 * 
462
	 * TODO: Make this generic for WHERE or JOIN clauses. prepareCondition()?
463
	 * 
464
	 * @param string $condition
465
	 * @return string
466
	 */
467
	protected function prepareJoinCondition($condition)
468
	{
469
		$parts = preg_split('/\s+/', $condition, 3);
470
		
471
		if (count($parts) < 3) {
472
			return null;
473
		}
474
		
475
		list($first, $operator, $second) = $parts;
476
		
477
		return static::concatenate(array(
478
			$this->identifier($first),
479
			$this->prepareRawOperator($operator),
480
			$this->identifier($second)
481
		));
482
	}
483
	
484
	/**
485
	 * Prepare a join's conditions.
486
	 * 
487
	 * @param Join $join
488
	 * @return string
489
	 */
490
	protected function prepareJoinConditions(Join $join)
491
	{
492
		$conditions = array();
493
		
494
		foreach ($join->conditions as $condition) {
495
			$conditions[] = $this->prepareJoinCondition($condition);
496
		}
497
		
498
		$conditions = array_merge($conditions, $this->prepareFilter($join->filter));
499
		
500
		return static::concatenate($conditions, ' AND ');
501
	}
502
	
503
	/**
504
	 * Prepare an individual table join.
505
	 * 
506
	 * @param Join $join
507
	 * @return string
508
	 */
509
	protected function prepareJoin(Join $join)
510
	{
511
		$table = $this->prepareJoinTable($join);
512
		$conditions = $this->prepareJoinConditions($join);
513
		
514
		$clause = $table && $conditions ? "$table ON $conditions" : $table;
515
		
516
		if (empty($clause)) {
517
			return null;
518
		}
519
		
520
		$type = $this->prepareJoinType($join->type);
521
		
522
		return "$type $clause";
523
	}
524
	
525
	/**
526
	 * Prepare table joins.
527
	 * 
528
	 * @param array $joins
529
	 * @return string
530
	 */
531
	protected function prepareJoins(array $joins)
532
	{
533
		$clauses = array();
534
		
535
		foreach ($joins as $join) {
536
			$clauses[] = $this->prepareJoin($join);
537
		}
538
		
539
		return static::concatenate($clauses);
540
	}
541
	
542
	/**
543
	 * Prepare an individual filter condition.
544
	 * 
545
	 * @param string $column
546
	 * @param mixed  $given
547
	 * @return string
548
	 */
549
	protected function prepareFilterCondition($column, $given)
550
	{
551
		list($left, $right) = array_pad(preg_split('/\s+/', $column, 2), 2, null);
552
		
553
		$column = $this->prepareColumns($left);
554
		
555
		$operator = $this->prepareOperator($right, $given);
556
		$value    = $this->value($given);
557
		
558
		// If the given value is null and whatever's on the right isn't a valid
559
		// operator we can attempt to split again and find a second identifier
560
		if ($given === null && !empty($right) && !$this->validOperator($right)) {
561
			list($operator, $identifier) = array_pad(preg_split('/\s+([\w\.]+)$/', $right, 2, PREG_SPLIT_DELIM_CAPTURE), 2, null);
562
			
563
			if (!empty($identifier)) {
564
				$operator = $this->prepareRawOperator($operator);
565
				$value    = $this->identifier($identifier);
566
			}
567
		}
568
		
569
		if (is_array($value)) {
570
			$value = "(" . implode(", ", $value) . ")";
571
		}
572
		
573
		return "$column $operator $value";
574
	}
575
	
576
	/**
577
	 * Prepare a filter as a set of query conditions.
578
	 * 
579
	 * TODO: Could numeric keys be dealt with by prepareJoinCondition()?
580
	 * 
581
	 * @param array $filter
582
	 * @return array
583
	 */
584
	protected function prepareFilter(array $filter)
585
	{
586
		$conditions = array();
587
		
588
		foreach ($filter as $column => $value) {
589
			if (strtolower($column) == 'or') {
590
				$conditions[] = '(' . $this->prepareWhere($value, 'OR', true) . ')';
591
			} else {
592
				$conditions[] = $this->prepareFilterCondition($column, $value);
593
			}
594
		}
595
		
596
		return $conditions;
597
	}
598
	
599
	/**
600
	 * Prepare a WHERE clause using the given filter and comparison operator.
601
	 * 
602
	 * Example filter key-values and their SQL equivalents:
603
	 *     'id'        => 1,       // id = '1'
604
	 *     'name like' => 'Chris', // name LIKE 'Chris'
605
	 *     'count >'   => 10,      // count > '10'
606
	 *     'type in'   => [1, 2],  // type IN (1, 2)
607
	 *     'type'      => [3, 4]   // type IN (3, 4)
608
	 * 
609
	 * Comparison operator between conditions defaults to 'AND'.
610
	 * 
611
	 * @param array  $filter
612
	 * @param string $comparison   [optional]
613
	 * @param bool   $excludeWhere [optional]
614
	 * @return string
615
	 */
616
	protected function prepareWhere(array $filter, $comparison = 'AND', $excludeWhere = false)
617
	{
618
		$conditions = $this->prepareFilter($filter);
619
		
620
		if (empty($conditions)) {
621
			return null;
622
		}
623
		
624
		$clause = implode(" $comparison ", $conditions);
625
		
626
		return !$excludeWhere ? "WHERE $clause" : $clause;
627
	}
628
	
629
	/**
630
	 * Prepare an individual order condition.
631
	 * 
632
	 * @param string $column
633
	 * @param string $direction [optional]
634
	 * @return string
635
	 */
636
	protected function prepareOrder($column, $direction = null)
637
	{
638
		$column = $this->identifier($column);
639
		$direction = $direction !== null ? strtoupper($direction) : 'ASC';
640
		
641
		return !empty($column) ? "$column $direction" : null;
642
	}
643
	
644
	/**
645
	 * Prepare an ORDER BY clause using the given order.
646
	 * 
647
	 * Example order key-values:
648
	 *     'column',
649
	 *     'other_column'   => 'ASC',
650
	 *     'another_column' => 'DESC
651
	 * 
652
	 * Ordered ascending by default.
653
	 * 
654
	 * @param array|string $order
655
	 * @return string
656
	 */
657
	protected function prepareOrderBy($order)
658
	{
659
		$conditions = array();
660
		
661
		foreach ((array) $order as $key => $value) {
662
			if (is_numeric($key)) {
663
				$conditions[] = $this->prepareOrder($value);
664
			} else {
665
				$conditions[] = $this->prepareOrder($key, $value);
666
			}
667
		}
668
		
669
		return count($conditions) ? 'ORDER BY ' . implode(', ', $conditions) : null;
670
	}
671
	
672
	/**
673
	 * Prepare a LIMIT clause using the given limit and offset.
674
	 * 
675
	 * @param int $limit  [optional]
676
	 * @param int $offset [optional]
677
	 * @return string
678
	 */
679
	abstract protected function prepareLimit($limit = 0, $offset = 0);
680
	
681
	/**
682
	 * Prepare a GROUP BY clause using the given groupings.
683
	 * 
684
	 * @param string[] $groupings
685
	 * @return string
686
	 */
687
	protected function prepareGroupBy(array $groupings)
688
	{
689
		return count($groupings) ? 'GROUP BY ' . implode(', ', $this->identifier($groupings)) : null;
690
	}
691
	
692
	/**
693
	 * Prepare a HAVING clause using the given filter.
694
	 * 
695
	 * @param array $filter
696
	 * @return string
697
	 */
698
	protected function prepareHaving(array $filter)
699
	{
700
		$clause = $this->prepareWhere($filter, 'AND', true);
701
		
702
		if (empty($clause)) {
703
			return null;
704
		}
705
		
706
		return "HAVING $clause";
707
	}
708
	
709
	/**
710
	 * Prepare a SELECT statement using the given columns, table, clauses and
711
	 * options.
712
	 * 
713
	 * TODO: Simplify this so that prepareSelect only actually prepares the
714
	 *       SELECT and FROM clauses. The rest could be concatenated by
715
	 *       translateRead().
716
	 * 
717
	 * @param string       $table
718
	 * @param array|string $columns
719
	 * @param string       $joins     [optional]
720
	 * @param string       $where     [optional]
721
	 * @param string       $order     [optional]
722
	 * @param string       $limit     [optional]
723
	 * @param string       $groupings [optional]
724
	 * @param string       $having    [optional]
725
	 * @param bool         $distinct  [optional]
726
	 * @return string
727
	 */
728
	abstract protected function prepareSelect(
729
		$table,
730
		$columns,
731
		$joins = null,
732
		$where = null,
733
		$order = null,
734
		$limit = null,
735
		$groupings = null,
736
		$having = null,
737
		$distinct = false
738
	);
739
	
740
	/**
741
	 * Prepare an INSERT INTO statement using the given table and data.
742
	 * 
743
	 * @param string $table
744
	 * @param array  $data
745
	 * @return string
746
	 */
747
	protected function prepareInsert($table, array $data)
748
	{
749
		$table = $this->identifier($table);
750
		
751
		$columns = $this->identifier(array_keys($data));
752
		$values  = $this->value(array_values($data));
753
		
754
		$columns = '(' . implode(', ', $columns) . ')';
755
		$values  = '(' . implode(', ', $values) . ')';
756
		
757
		return static::concatenate(array('INSERT INTO', $table, $columns, 'VALUES', $values));
758
	}
759
	
760
	/**
761
	 * Prepare an INSERT SELECT statement using the given table and
762
	 * subquery.
763
	 * 
764
	 * @param string        $table
765
	 * @param array         $columns
766
	 * @param Storage\Query $subquery
767
	 * @return string
768
	 */
769
	public function prepareInsertSelect($table, array $columns, Storage\Query $subquery)
770
	{
771
		$table = $this->identifier($table);
772
		
773
		if (!empty($columns)) {
774
			$columns = $this->identifier($columns);
775
			$columns = "(" . implode(", ", $columns) . ")";
776
		}
777
		
778
		$subquery = (string) $this->translate($subquery);
779
		
780
		return static::concatenate(array('INSERT INTO', $table, $columns, $subquery));
781
	}
782
	
783
	/**
784
	 * Prepare an UPDATE statement with the given table, data and clauses.
785
	 * 
786
	 * @param string $table
787
	 * @param array  $data
788
	 * @param string $where [optional]
789
	 * @param string $limit [optional]
790
	 * @return string
791
	 */
792
	abstract protected function prepareUpdate($table, $data, $where = null, $limit = null);
793
	
794
	/**
795
	 * Prepare a DELETE statement with the given table and clauses.
796
	 * 
797
	 * @param string $table
798
	 * @param string $where [optional]
799
	 * @param string $limit [optional]
800
	 * @return string
801
	 */
802
	abstract protected function prepareDelete($table, $where = null, $limit = null);
803
	
804
	/**
805
	 * Prepare a set of query parameters from the given set of columns.
806
	 * 
807
	 * @param array $columns
808
	 * @return array
809
	 */
810
	protected function columnParameters($columns)
811
	{
812
		$parameters = array();
813
		
814
		foreach ($columns as $column) {
815
			if ($column instanceof Storage\Query\Builder) {
816
				$column = $column->query;
817
			}
818
			
819
			if ($column instanceof Storage\Query) {
820
				$parameters = array_merge($parameters, $this->parameters($column));
821
			}
822
		}
823
		
824
		return $parameters;
825
	}
826
	
827
	/**
828
	 * Prepare a set of query parameters from the given data.
829
	 * 
830
	 * @param array $data
831
	 * @return array
832
	 */
833
	protected function dataParameters($data)
834
	{
835
		$parameters = array();
836
		
837
		foreach ($data as $value) {
838
			if ($this->resolvesPlaceholder($value)) {
839
				$parameters[] = $value;
840
			}
841
		}
842
		
843
		return $parameters;
844
	}
845
	
846
	/**
847
	 * Prepare a set of query parameters from the given set of joins.
848
	 * 
849
	 * @param Join[] $joins
850
	 * @return array
851
	 */
852
	protected function joinParameters($joins)
853
	{
854
		$parameters = array();
855
		
856
		foreach ($joins as $join) {
857
			$parameters = array_merge($parameters, $this->filterParameters($join->filter));
858
		}
859
		
860
		return $parameters;
861
	}
862
	
863
	/**
864
	 * Prepare a set of query parameters from the given filter.
865
	 * 
866
	 * @param array $filter
867
	 * @return array
868
	 */
869
	protected function filterParameters($filter)
870
	{
871
		$parameters = array();
872
		
873
		foreach ($filter as $index => $value) {
874
			if (is_array($value)) {
875
				if (strtolower($index) === 'or') {
876
					$parameters = array_merge($parameters, $this->filterParameters($value));
877
				} else {
878
					foreach ($value as $in) {
879
						if ($this->resolvesPlaceholder($value)) {
880
							$parameters[] = $in;
881
						}
882
					}
883
				}
884
				
885
				continue;
886
			}
887
			
888
			if ($value instanceof Storage\Query\Builder) {
889
				$value = $value->query;
890
			}
891
			
892
			if ($value instanceof Storage\Query) {
893
				$parameters = array_merge($parameters, $this->parameters($value));
894
				
895
				continue;
896
			}
897
			
898
			if ($this->resolvesPlaceholder($value)) {
899
				$parameters[] = $value;
900
			}
901
		}
902
		
903
		return $parameters;
904
	}
905
	
906
	/**
907
	 * Retrieve an array of parameters from the given query for executing a
908
	 * prepared query.
909
	 * 
910
	 * @param Storage\Query $storageQuery
911
	 * @return array
912
	 */
913
	public function parameters(Storage\Query $storageQuery)
914
	{
915
		$parameters = $this->columnParameters($storageQuery->fields);
916
		
917
		if (in_array($storageQuery->type, array(Storage\Query::CREATE, Storage\Query::UPDATE))) {
918
			$parameters = $this->dataParameters($storageQuery->data);
919
		}
920
		
921
		$joinParameters = array();
922
		$havingParameters = array();
923
		
924
		if ($storageQuery instanceof Database\Storage\Query) {
925
			$joinParameters = $this->joinParameters($storageQuery->joins);
926
			$havingParameters = $this->filterParameters($storageQuery->having);
927
		}
928
		
929
		$parameters = array_merge(
930
			$parameters,
931
			$joinParameters,
932
			$this->filterParameters($storageQuery->filter),
933
			$havingParameters
934
		);
935
		
936
		return $parameters;
937
	}
938
}
939