Issues (137)

src/SphinxQL.php (2 issues)

1
<?php
2
3
namespace Foolz\SphinxQL;
4
5
use Foolz\SphinxQL\Drivers\ConnectionInterface;
6
use Foolz\SphinxQL\Drivers\MultiResultSetInterface;
7
use Foolz\SphinxQL\Drivers\ResultSetInterface;
8
use Foolz\SphinxQL\Exception\ConnectionException;
9
use Foolz\SphinxQL\Exception\DatabaseException;
10
use Foolz\SphinxQL\Exception\SphinxQLException;
11
12
/**
13
 * Query Builder class for SphinxQL statements.
14
 */
15
class SphinxQL
16
{
17
    /**
18
     * A non-static connection for the current SphinxQL object
19
     *
20
     * @var ConnectionInterface
21
     */
22
    protected $connection;
23
24
    /**
25
     * The last result object.
26
     *
27
     * @var array
28
     */
29
    protected $last_result;
30
31
    /**
32
     * The last compiled query.
33
     *
34
     * @var string
35
     */
36
    protected $last_compiled;
37
38
    /**
39
     * The last chosen method (select, insert, replace, update, delete).
40
     *
41
     * @var string
42
     */
43
    protected $type;
44
45
    /**
46
     * An SQL query that is not yet executed or "compiled"
47
     *
48
     * @var string
49
     */
50
    protected $query;
51
52
    /**
53
     * Array of select elements that will be comma separated.
54
     *
55
     * @var array
56
     */
57
    protected $select = array();
58
59
    /**
60
     * From in SphinxQL is the list of indexes that will be used
61
     *
62
     * @var array
63
     */
64
    protected $from = array();
65
66
    /**
67
     * The list of where and parenthesis, must be inserted in order
68
     *
69
     * @var array
70
     */
71
    protected $where = array();
72
73
    /**
74
     * The list of matches for the MATCH function in SphinxQL
75
     *
76
     * @var array
77
     */
78
    protected $match = array();
79
80
    /**
81
     * GROUP BY array to be comma separated
82
     *
83
     * @var array
84
     */
85
    protected $group_by = array();
86
87
    /**
88
     * When not null changes 'GROUP BY' to 'GROUP N BY'
89
     *
90
     * @var null|int
91
     */
92
    protected $group_n_by;
93
94
    /**
95
     * ORDER BY array
96
     *
97
     * @var array
98
     */
99
    protected $within_group_order_by = array();
100
101
    /**
102
     * The list of where and parenthesis, must be inserted in order
103
     *
104
     * @var array
105
     */
106
    protected $having = array();
107
108
    /**
109
     * ORDER BY array
110
     *
111
     * @var array
112
     */
113
    protected $order_by = array();
114
115
    /**
116
     * When not null it adds an offset
117
     *
118
     * @var null|int
119
     */
120
    protected $offset;
121
122
    /**
123
     * When not null it adds a limit
124
     *
125
     * @var null|int
126
     */
127
    protected $limit;
128
129
    /**
130
     * Value of INTO query for INSERT or REPLACE
131
     *
132
     * @var null|string
133
     */
134
    protected $into;
135
136
    /**
137
     * Array of columns for INSERT or REPLACE
138
     *
139
     * @var array
140
     */
141
    protected $columns = array();
142
143
    /**
144
     * Array OF ARRAYS of values for INSERT or REPLACE
145
     *
146
     * @var array
147
     */
148
    protected $values = array();
149
150
    /**
151
     * Array arrays containing column and value for SET in UPDATE
152
     *
153
     * @var array
154
     */
155
    protected $set = array();
156
157
    /**
158
     * Array of OPTION specific to SphinxQL
159
     *
160
     * @var array
161
     */
162
    protected $options = array();
163
164
    /**
165
     * Array of FACETs
166
     *
167
     * @var Facet[]
168
     */
169
    protected $facets = array();
170
171
    /**
172
     * The reference to the object that queued itself and created this object
173
     *
174
     * @var null|SphinxQL
175
     */
176
    protected $queue_prev;
177
178
    /**
179
     * An array of escaped characters for escapeMatch()
180
     * @var array
181
     */
182
    protected $escape_full_chars = array(
183
        '\\' => '\\\\',
184
        '('  => '\(',
185
        ')'  => '\)',
186
        '|'  => '\|',
187
        '-'  => '\-',
188
        '!'  => '\!',
189
        '@'  => '\@',
190
        '~'  => '\~',
191
        '"'  => '\"',
192
        '&'  => '\&',
193
        '/'  => '\/',
194
        '^'  => '\^',
195
        '$'  => '\$',
196
        '='  => '\=',
197
        '<'  => '\<',
198
    );
199
200
    /**
201
     * An array of escaped characters for fullEscapeMatch()
202
     * @var array
203
     */
204
    protected $escape_half_chars = array(
205
        '\\' => '\\\\',
206
        '('  => '\(',
207
        ')'  => '\)',
208
        '!'  => '\!',
209
        '@'  => '\@',
210
        '~'  => '\~',
211
        '&'  => '\&',
212
        '/'  => '\/',
213
        '^'  => '\^',
214
        '$'  => '\$',
215
        '='  => '\=',
216
        '<'  => '\<',
217
    );
218
219
    /**
220
     * @param ConnectionInterface|null $connection
221
     */
222
    public function __construct(ConnectionInterface $connection = null)
223
    {
224
        $this->connection = $connection;
225
    }
226
    
227
    /**
228
     * Sets Query Type
229
     *
230
     */
231
    public function setType(string $type)
232
    {
233
        return $this->type = $type;
234
    }    
235
236
    /**
237
     * Returns the currently attached connection
238
     *
239
     * @returns ConnectionInterface
240
     */
241
    public function getConnection()
242
    {
243
        return $this->connection;
244
    }
245
246
    /**
247
     * Avoids having the expressions escaped
248
     *
249
     * Examples:
250
     *    $query->where('time', '>', SphinxQL::expr('CURRENT_TIMESTAMP'));
251
     *    // WHERE time > CURRENT_TIMESTAMP
252
     *
253
     * @param string $string The string to keep unaltered
254
     *
255
     * @return Expression The new Expression
256
     * @todo make non static
257
     */
258
    public static function expr($string = '')
259
    {
260
        return new Expression($string);
261
    }
262
263
    /**
264
     * Runs the query built
265
     *
266
     * @return ResultSetInterface The result of the query
267
     * @throws DatabaseException
268
     * @throws ConnectionException
269
     * @throws SphinxQLException
270
     */
271
    public function execute()
272
    {
273
        // pass the object so execute compiles it by itself
274
        return $this->last_result = $this->getConnection()->query($this->compile()->getCompiled());
275
    }
276
277
    /**
278
     * Executes a batch of queued queries
279
     *
280
     * @return MultiResultSetInterface The array of results
281
     * @throws SphinxQLException In case no query is in queue
282
     * @throws Exception\DatabaseException
283
     * @throws ConnectionException
284
     */
285
    public function executeBatch()
286
    {
287
        if (count($this->getQueue()) == 0) {
288
            throw new SphinxQLException('There is no Queue present to execute.');
289
        }
290
291
        $queue = array();
292
293
        foreach ($this->getQueue() as $query) {
294
            $queue[] = $query->compile()->getCompiled();
295
        }
296
297
        return $this->last_result = $this->getConnection()->multiQuery($queue);
298
    }
299
300
    /**
301
     * Enqueues the current object and returns a new one or the supplied one
302
     *
303
     * @param SphinxQL|null $next
304
     *
305
     * @return SphinxQL A new SphinxQL object with the current object referenced
306
     */
307
    public function enqueue(SphinxQL $next = null)
308
    {
309
        if ($next === null) {
310
            $next = new static($this->getConnection());
311
        }
312
313
        $next->setQueuePrev($this);
314
315
        return $next;
316
    }
317
318
    /**
319
     * Returns the ordered array of enqueued objects
320
     *
321
     * @return SphinxQL[] The ordered array of enqueued objects
322
     */
323
    public function getQueue()
324
    {
325
        $queue = array();
326
        $curr = $this;
327
328
        do {
329
            if ($curr->type != null) {
330
                $queue[] = $curr;
331
            }
332
        } while ($curr = $curr->getQueuePrev());
333
334
        return array_reverse($queue);
335
    }
336
337
    /**
338
     * Gets the enqueued object
339
     *
340
     * @return SphinxQL|null
341
     */
342
    public function getQueuePrev()
343
    {
344
        return $this->queue_prev;
345
    }
346
347
    /**
348
     * Sets the reference to the enqueued object
349
     *
350
     * @param SphinxQL $query The object to set as previous
351
     *
352
     * @return self
353
     */
354
    public function setQueuePrev($query)
355
    {
356
        $this->queue_prev = $query;
357
358
        return $this;
359
    }
360
361
    /**
362
     * Returns the result of the last query
363
     *
364
     * @return array The result of the last query
365
     */
366
    public function getResult()
367
    {
368
        return $this->last_result;
369
    }
370
371
    /**
372
     * Returns the latest compiled query
373
     *
374
     * @return string The last compiled query
375
     */
376
    public function getCompiled()
377
    {
378
        return $this->last_compiled;
379
    }
380
381
    /**
382
     * Begins transaction
383
     * @throws DatabaseException
384
     * @throws ConnectionException
385
     */
386
    public function transactionBegin()
387
    {
388
        $this->getConnection()->query('BEGIN');
389
    }
390
391
    /**
392
     * Commits transaction
393
     * @throws DatabaseException
394
     * @throws ConnectionException
395
     */
396
    public function transactionCommit()
397
    {
398
        $this->getConnection()->query('COMMIT');
399
    }
400
401
    /**
402
     * Rollbacks transaction
403
     * @throws DatabaseException
404
     * @throws ConnectionException
405
     */
406
    public function transactionRollback()
407
    {
408
        $this->getConnection()->query('ROLLBACK');
409
    }
410
411
    /**
412
     * Runs the compile function
413
     *
414
     * @return self
415
     * @throws ConnectionException
416
     * @throws DatabaseException
417
     * @throws SphinxQLException
418
     */
419
    public function compile()
420
    {
421
        switch ($this->type) {
422
            case 'select':
423
                $this->compileSelect();
424
                break;
425
            case 'insert':
426
            case 'replace':
427
                $this->compileInsert();
428
                break;
429
            case 'update':
430
                $this->compileUpdate();
431
                break;
432
            case 'delete':
433
                $this->compileDelete();
434
                break;
435
            case 'query':
436
                $this->compileQuery();
437
                break;
438
        }
439
440
        return $this;
441
    }
442
443
    /**
444
     * @return self
445
     */
446
    public function compileQuery()
447
    {
448
        $this->last_compiled = $this->query;
449
450
        return $this;
451
    }
452
453
    /**
454
     * Compiles the MATCH part of the queries
455
     * Used by: SELECT, DELETE, UPDATE
456
     *
457
     * @return string The compiled MATCH
458
     * @throws Exception\ConnectionException
459
     * @throws Exception\DatabaseException
460
     */
461
    public function compileMatch()
462
    {
463
        $query = '';
464
465
        if (!empty($this->match)) {
466
            $query .= 'WHERE MATCH(';
467
468
            $matched = array();
469
470
            foreach ($this->match as $match) {
471
                $pre = '';
472
                if ($match['column'] instanceof \Closure) {
473
                    $sub = new MatchBuilder($this);
474
                    call_user_func($match['column'], $sub);
475
                    $pre .= $sub->compile()->getCompiled();
476
                } elseif ($match['column'] instanceof MatchBuilder) {
477
                    $pre .= $match['column']->compile()->getCompiled();
478
                } elseif (empty($match['column'])) {
479
                    $pre .= '';
480
                } elseif (is_array($match['column'])) {
481
                    $pre .= '@('.implode(',', $match['column']).') ';
482
                } else {
483
                    $pre .= '@'.$match['column'].' ';
484
                }
485
486
                if ($match['half']) {
487
                    $pre .= $this->halfEscapeMatch($match['value']);
488
                } else {
489
                    $pre .= $this->escapeMatch($match['value']);
490
                }
491
492
                if ($pre !== '') {
493
                    $matched[] = '('.$pre.')';
494
                }
495
            }
496
497
            $matched = implode(' ', $matched);
498
            $query .= $this->getConnection()->escape(trim($matched)).') ';
499
        }
500
501
        return $query;
502
    }
503
504
    /**
505
     * Compiles the WHERE part of the queries
506
     * It interacts with the MATCH() and of course isn't usable stand-alone
507
     * Used by: SELECT, DELETE, UPDATE
508
     *
509
     * @return string The compiled WHERE
510
     * @throws ConnectionException
511
     * @throws DatabaseException
512
     */
513
    public function compileWhere()
514
    {
515
        $query = '';
516
517
        if (empty($this->match) && !empty($this->where)) {
518
            $query .= 'WHERE ';
519
        }
520
521
        if (!empty($this->where)) {
522
            foreach ($this->where as $key => $where) {
523
                if ($key > 0 || !empty($this->match)) {
524
                    $query .= 'AND ';
525
                }
526
                $query .= $this->compileFilterCondition($where);
527
            }
528
        }
529
530
        return $query;
531
    }
532
533
    /**
534
     * @param array $filter
535
     *
536
     * @return string
537
     * @throws ConnectionException
538
     * @throws DatabaseException
539
     */
540
    public function compileFilterCondition($filter)
541
    {
542
        $query = '';
543
544
        if (!empty($filter)) {
545
            if (strtoupper($filter['operator']) === 'BETWEEN') {
546
                $query .= $filter['column'];
547
                $query .= ' BETWEEN ';
548
                $query .= $this->getConnection()->quote($filter['value'][0]).' AND '
549
                    .$this->getConnection()->quote($filter['value'][1]).' ';
550
            } else {
551
                // id can't be quoted!
552
                if ($filter['column'] === 'id') {
553
                    $query .= 'id ';
554
                } else {
555
                    $query .= $filter['column'].' ';
556
                }
557
558
                if (in_array(strtoupper($filter['operator']), array('IN', 'NOT IN'), true)) {
559
                    $query .= strtoupper($filter['operator']).' ('.implode(', ', $this->getConnection()->quoteArr($filter['value'])).') ';
560
                } else {
561
                    $query .= $filter['operator'].' '.$this->getConnection()->quote($filter['value']).' ';
562
                }
563
            }
564
        }
565
566
        return $query;
567
    }
568
569
    /**
570
     * Compiles the statements for SELECT
571
     *
572
     * @return self
573
     * @throws ConnectionException
574
     * @throws DatabaseException
575
     * @throws SphinxQLException
576
     */
577
    public function compileSelect()
578
    {
579
        $query = '';
580
581
        if ($this->type == 'select') {
582
            $query .= 'SELECT ';
583
584
            if (!empty($this->select)) {
585
                $query .= implode(', ', $this->select).' ';
586
            } else {
587
                $query .= '* ';
588
            }
589
        }
590
591
        if (!empty($this->from)) {
592
            if ($this->from instanceof \Closure) {
0 ignored issues
show
$this->from is never a sub-type of Closure.
Loading history...
593
                $sub = new static($this->getConnection());
594
                call_user_func($this->from, $sub);
595
                $query .= 'FROM ('.$sub->compile()->getCompiled().') ';
596
            } elseif ($this->from instanceof SphinxQL) {
0 ignored issues
show
$this->from is never a sub-type of Foolz\SphinxQL\SphinxQL.
Loading history...
597
                $query .= 'FROM ('.$this->from->compile()->getCompiled().') ';
598
            } else {
599
                $query .= 'FROM '.implode(', ', $this->from).' ';
600
            }
601
        }
602
603
        $query .= $this->compileMatch().$this->compileWhere();
604
605
        if (!empty($this->group_by)) {
606
            $query .= 'GROUP ';
607
            if ($this->group_n_by !== null) {
608
                $query .= $this->group_n_by.' ';
609
            }
610
            $query .= 'BY '.implode(', ', $this->group_by).' ';
611
        }
612
613
        if (!empty($this->within_group_order_by)) {
614
            $query .= 'WITHIN GROUP ORDER BY ';
615
616
            $order_arr = array();
617
618
            foreach ($this->within_group_order_by as $order) {
619
                $order_sub = $order['column'].' ';
620
621
                if ($order['direction'] !== null) {
622
                    $order_sub .= ((strtolower($order['direction']) === 'desc') ? 'DESC' : 'ASC');
623
                }
624
625
                $order_arr[] = $order_sub;
626
            }
627
628
            $query .= implode(', ', $order_arr).' ';
629
        }
630
631
        if (!empty($this->having)) {
632
            $query .= 'HAVING '.$this->compileFilterCondition($this->having);
633
        }
634
635
        if (!empty($this->order_by)) {
636
            $query .= 'ORDER BY ';
637
638
            $order_arr = array();
639
640
            foreach ($this->order_by as $order) {
641
                $order_sub = $order['column'].' ';
642
643
                if ($order['direction'] !== null) {
644
                    $order_sub .= ((strtolower($order['direction']) === 'desc') ? 'DESC' : 'ASC');
645
                }
646
647
                $order_arr[] = $order_sub;
648
            }
649
650
            $query .= implode(', ', $order_arr).' ';
651
        }
652
653
        if ($this->limit !== null || $this->offset !== null) {
654
            if ($this->offset === null) {
655
                $this->offset = 0;
656
            }
657
658
            if ($this->limit === null) {
659
                $this->limit = 9999999999999;
660
            }
661
662
            $query .= 'LIMIT '.((int) $this->offset).', '.((int) $this->limit).' ';
663
        }
664
665
        if (!empty($this->options)) {
666
            $options = array();
667
668
            foreach ($this->options as $option) {
669
                if ($option['value'] instanceof Expression) {
670
                    $option['value'] = $option['value']->value();
671
                } elseif (is_array($option['value'])) {
672
                    array_walk(
673
                        $option['value'],
674
                        function (&$val, $key) {
675
                            $val = $key.'='.$val;
676
                        }
677
                    );
678
                    $option['value'] = '('.implode(', ', $option['value']).')';
679
                } else {
680
                    $option['value'] = $this->getConnection()->quote($option['value']);
681
                }
682
683
                $options[] = $option['name'].' = '.$option['value'];
684
            }
685
686
            $query .= 'OPTION '.implode(', ', $options).' ';
687
        }
688
689
        if (!empty($this->facets)) {
690
            $facets = array();
691
692
            foreach ($this->facets as $facet) {
693
                // dynamically set the own SphinxQL connection if the Facet doesn't own one
694
                if ($facet->getConnection() === null) {
695
                    $facet->setConnection($this->getConnection());
696
                    $facets[] = $facet->getFacet();
697
                    // go back to the status quo for reuse
698
                    $facet->setConnection();
699
                } else {
700
                    $facets[] = $facet->getFacet();
701
                }
702
            }
703
704
            $query .= implode(' ', $facets);
705
        }
706
707
        $query = trim($query);
708
        $this->last_compiled = $query;
709
710
        return $this;
711
    }
712
713
    /**
714
     * Compiles the statements for INSERT or REPLACE
715
     *
716
     * @return self
717
     * @throws ConnectionException
718
     * @throws DatabaseException
719
     */
720
    public function compileInsert()
721
    {
722
        if ($this->type == 'insert') {
723
            $query = 'INSERT ';
724
        } else {
725
            $query = 'REPLACE ';
726
        }
727
728
        if ($this->into !== null) {
729
            $query .= 'INTO '.$this->into.' ';
730
        }
731
732
        if (!empty($this->columns)) {
733
            $query .= '('.implode(', ', $this->columns).') ';
734
        }
735
736
        if (!empty($this->values)) {
737
            $query .= 'VALUES ';
738
            $query_sub = array();
739
740
            foreach ($this->values as $value) {
741
                $query_sub[] = '('.implode(', ', $this->getConnection()->quoteArr($value)).')';
742
            }
743
744
            $query .= implode(', ', $query_sub);
745
        }
746
747
        $query = trim($query);
748
        $this->last_compiled = $query;
749
750
        return $this;
751
    }
752
753
    /**
754
     * Compiles the statements for UPDATE
755
     *
756
     * @return self
757
     * @throws ConnectionException
758
     * @throws DatabaseException
759
     */
760
    public function compileUpdate()
761
    {
762
        $query = 'UPDATE ';
763
764
        if ($this->into !== null) {
765
            $query .= $this->into.' ';
766
        }
767
768
        if (!empty($this->set)) {
769
            $query .= 'SET ';
770
771
            $query_sub = array();
772
773
            foreach ($this->set as $column => $value) {
774
                // MVA support
775
                if (is_array($value)) {
776
                    $query_sub[] = $column
777
                        .' = ('.implode(', ', $this->getConnection()->quoteArr($value)).')';
778
                } else {
779
                    $query_sub[] = $column
780
                        .' = '.$this->getConnection()->quote($value);
781
                }
782
            }
783
784
            $query .= implode(', ', $query_sub).' ';
785
        }
786
787
        $query .= $this->compileMatch().$this->compileWhere();
788
789
        $query = trim($query);
790
        $this->last_compiled = $query;
791
792
        return $this;
793
    }
794
795
    /**
796
     * Compiles the statements for DELETE
797
     *
798
     * @return self
799
     * @throws ConnectionException
800
     * @throws DatabaseException
801
     */
802
    public function compileDelete()
803
    {
804
        $query = 'DELETE ';
805
806
        if (!empty($this->from)) {
807
            $query .= 'FROM '.$this->from[0].' ';
808
        }
809
810
        if (!empty($this->match)) {
811
            $query .= $this->compileMatch();
812
        }
813
        if (!empty($this->where)) {
814
            $query .= $this->compileWhere();
815
        }
816
817
        $query = trim($query);
818
        $this->last_compiled = $query;
819
820
        return $this;
821
    }
822
823
    /**
824
     * Sets a query to be executed
825
     *
826
     * @param string $sql A SphinxQL query to execute
827
     *
828
     * @return self
829
     */
830
    public function query($sql)
831
    {
832
        $this->type = 'query';
833
        $this->query = $sql;
834
835
        return $this;
836
    }
837
838
    /**
839
     * Select the columns
840
     *
841
     * Gets the arguments passed as $sphinxql->select('one', 'two')
842
     * Using it without arguments equals to having '*' as argument
843
     * Using it with array maps values as column names
844
     *
845
     * Examples:
846
     *    $query->select('title');
847
     *    // SELECT title
848
     *
849
     *    $query->select('title', 'author', 'date');
850
     *    // SELECT title, author, date
851
     *
852
     *    $query->select(['id', 'title']);
853
     *    // SELECT id, title
854
     *
855
     * @param array|string $columns Array or multiple string arguments containing column names
856
     *
857
     * @return self
858
     */
859
    public function select($columns = null)
860
    {
861
        $this->reset();
862
        $this->type = 'select';
863
864
        if (is_array($columns)) {
865
            $this->select = $columns;
866
        } else {
867
            $this->select = \func_get_args();
868
        }
869
870
        return $this;
871
    }
872
873
    /**
874
     * Alters which arguments to select
875
     *
876
     * Query is assumed to be in SELECT mode
877
     * See select() for usage
878
     *
879
     * @param array|string $columns Array or multiple string arguments containing column names
880
     *
881
     * @return self
882
     */
883
    public function setSelect($columns = null)
884
    {
885
        if (is_array($columns)) {
886
            $this->select = $columns;
887
        } else {
888
            $this->select = \func_get_args();
889
        }
890
891
        return $this;
892
    }
893
894
    /**
895
     * Get the columns staged to select
896
     *
897
     * @return array
898
     */
899
    public function getSelect()
900
    {
901
        return $this->select;
902
    }
903
904
    /**
905
     * Activates the INSERT mode
906
     *
907
     * @return self
908
     */
909
    public function insert()
910
    {
911
        $this->reset();
912
        $this->type = 'insert';
913
914
        return $this;
915
    }
916
917
    /**
918
     * Activates the REPLACE mode
919
     *
920
     * @return self
921
     */
922
    public function replace()
923
    {
924
        $this->reset();
925
        $this->type = 'replace';
926
927
        return $this;
928
    }
929
930
    /**
931
     * Activates the UPDATE mode
932
     *
933
     * @param string $index The index to update into
934
     *
935
     * @return self
936
     */
937
    public function update($index)
938
    {
939
        $this->reset();
940
        $this->type = 'update';
941
        $this->into($index);
942
943
        return $this;
944
    }
945
946
    /**
947
     * Activates the DELETE mode
948
     *
949
     * @return self
950
     */
951
    public function delete()
952
    {
953
        $this->reset();
954
        $this->type = 'delete';
955
956
        return $this;
957
    }
958
959
    /**
960
     * FROM clause (Sphinx-specific since it works with multiple indexes)
961
     * func_get_args()-enabled
962
     *
963
     * @param array $array An array of indexes to use
964
     *
965
     * @return self
966
     */
967
    public function from($array = null)
968
    {
969
        if (is_string($array)) {
970
            $this->from = \func_get_args();
971
        }
972
973
        if (is_array($array) || $array instanceof \Closure || $array instanceof SphinxQL) {
974
            $this->from = $array;
975
        }
976
977
        return $this;
978
    }
979
980
    /**
981
     * MATCH clause (Sphinx-specific)
982
     *
983
     * @param mixed  $column The column name (can be array, string, Closure, or MatchBuilder)
984
     * @param string $value  The value
985
     * @param bool   $half   Exclude ", |, - control characters from being escaped
986
     *
987
     * @return self
988
     */
989
    public function match($column, $value = null, $half = false)
990
    {
991
        if ($column === '*' || (is_array($column) && in_array('*', $column))) {
992
            $column = array();
993
        }
994
995
        $this->match[] = array('column' => $column, 'value' => $value, 'half' => $half);
996
997
        return $this;
998
    }
999
1000
    /**
1001
     * WHERE clause
1002
     *
1003
     * Examples:
1004
     *    $query->where('column', 'value');
1005
     *    // WHERE column = 'value'
1006
     *
1007
     *    $query->where('column', '=', 'value');
1008
     *    // WHERE column = 'value'
1009
     *
1010
     *    $query->where('column', '>=', 'value')
1011
     *    // WHERE column >= 'value'
1012
     *
1013
     *    $query->where('column', 'IN', array('value1', 'value2', 'value3'));
1014
     *    // WHERE column IN ('value1', 'value2', 'value3')
1015
     *
1016
     *    $query->where('column', 'BETWEEN', array('value1', 'value2'))
1017
     *    // WHERE column BETWEEN 'value1' AND 'value2'
1018
     *    // WHERE example BETWEEN 10 AND 100
1019
     *
1020
     * @param string                                      $column   The column name
1021
     * @param Expression|string|null|bool|array|int|float $operator The operator to use (if value is not null, you can
1022
     *      use only string)
1023
     * @param Expression|string|null|bool|array|int|float $value    The value to check against
1024
     *
1025
     * @return self
1026
     */
1027
    public function where($column, $operator, $value = null)
1028
    {
1029
        if ($value === null) {
1030
            $value = $operator;
1031
            $operator = '=';
1032
        }
1033
1034
        $this->where[] = array(
1035
            'column'   => $column,
1036
            'operator' => $operator,
1037
            'value'    => $value,
1038
        );
1039
1040
        return $this;
1041
    }
1042
1043
    /**
1044
     * GROUP BY clause
1045
     * Adds to the previously added columns
1046
     *
1047
     * @param string $column A column to group by
1048
     *
1049
     * @return self
1050
     */
1051
    public function groupBy($column)
1052
    {
1053
        $this->group_by[] = $column;
1054
1055
        return $this;
1056
    }
1057
1058
    /**
1059
     * GROUP N BY clause (SphinxQL-specific)
1060
     * Changes 'GROUP BY' into 'GROUP N BY'
1061
     *
1062
     * @param int $n Number of items per group
1063
     *
1064
     * @return self
1065
     */
1066
    public function groupNBy($n)
1067
    {
1068
        $this->group_n_by = (int) $n;
1069
1070
        return $this;
1071
    }
1072
1073
    /**
1074
     * WITHIN GROUP ORDER BY clause (SphinxQL-specific)
1075
     * Adds to the previously added columns
1076
     * Works just like a classic ORDER BY
1077
     *
1078
     * @param string $column    The column to group by
1079
     * @param string $direction The group by direction (asc/desc)
1080
     *
1081
     * @return self
1082
     */
1083
    public function withinGroupOrderBy($column, $direction = null)
1084
    {
1085
        $this->within_group_order_by[] = array('column' => $column, 'direction' => $direction);
1086
1087
        return $this;
1088
    }
1089
1090
    /**
1091
     * HAVING clause
1092
     *
1093
     * Examples:
1094
     *    $sq->having('column', 'value');
1095
     *    // HAVING column = 'value'
1096
     *
1097
     *    $sq->having('column', '=', 'value');
1098
     *    // HAVING column = 'value'
1099
     *
1100
     *    $sq->having('column', '>=', 'value')
1101
     *    // HAVING column >= 'value'
1102
     *
1103
     *    $sq->having('column', 'IN', array('value1', 'value2', 'value3'));
1104
     *    // HAVING column IN ('value1', 'value2', 'value3')
1105
     *
1106
     *    $sq->having('column', 'BETWEEN', array('value1', 'value2'))
1107
     *    // HAVING column BETWEEN 'value1' AND 'value2'
1108
     *    // HAVING example BETWEEN 10 AND 100
1109
     *
1110
     * @param string $column   The column name
1111
     * @param string $operator The operator to use
1112
     * @param string $value    The value to check against
1113
     *
1114
     * @return self
1115
     */
1116
    public function having($column, $operator, $value = null)
1117
    {
1118
        if ($value === null) {
1119
            $value = $operator;
1120
            $operator = '=';
1121
        }
1122
1123
        $this->having = array(
1124
            'column'   => $column,
1125
            'operator' => $operator,
1126
            'value'    => $value,
1127
        );
1128
1129
        return $this;
1130
    }
1131
1132
    /**
1133
     * ORDER BY clause
1134
     * Adds to the previously added columns
1135
     *
1136
     * @param string $column    The column to order on
1137
     * @param string $direction The ordering direction (asc/desc)
1138
     *
1139
     * @return self
1140
     */
1141
    public function orderBy($column, $direction = null)
1142
    {
1143
        $this->order_by[] = array('column' => $column, 'direction' => $direction);
1144
1145
        return $this;
1146
    }
1147
1148
    /**
1149
     * LIMIT clause
1150
     * Supports also LIMIT offset, limit
1151
     *
1152
     * @param int      $offset Offset if $limit is specified, else limit
1153
     * @param null|int $limit  The limit to set, null for no limit
1154
     *
1155
     * @return self
1156
     */
1157
    public function limit($offset, $limit = null)
1158
    {
1159
        if ($limit === null) {
1160
            $this->limit = (int) $offset;
1161
1162
            return $this;
1163
        }
1164
1165
        $this->offset($offset);
1166
        $this->limit = (int) $limit;
1167
1168
        return $this;
1169
    }
1170
1171
    /**
1172
     * OFFSET clause
1173
     *
1174
     * @param int $offset The offset
1175
     *
1176
     * @return self
1177
     */
1178
    public function offset($offset)
1179
    {
1180
        $this->offset = (int) $offset;
1181
1182
        return $this;
1183
    }
1184
1185
    /**
1186
     * OPTION clause (SphinxQL-specific)
1187
     * Used by: SELECT
1188
     *
1189
     * @param string                                      $name  Option name
1190
     * @param Expression|array|string|int|bool|float|null $value Option value
1191
     *
1192
     * @return self
1193
     */
1194
    public function option($name, $value)
1195
    {
1196
        $this->options[] = array('name' => $name, 'value' => $value);
1197
1198
        return $this;
1199
    }
1200
1201
    /**
1202
     * INTO clause
1203
     * Used by: INSERT, REPLACE
1204
     *
1205
     * @param string $index The index to insert/replace into
1206
     *
1207
     * @return self
1208
     */
1209
    public function into($index)
1210
    {
1211
        $this->into = $index;
1212
1213
        return $this;
1214
    }
1215
1216
    /**
1217
     * Set columns
1218
     * Used in: INSERT, REPLACE
1219
     * func_get_args()-enabled
1220
     *
1221
     * @param array $array The array of columns
1222
     *
1223
     * @return self
1224
     */
1225
    public function columns($array = array())
1226
    {
1227
        if (is_array($array)) {
1228
            $this->columns = $array;
1229
        } else {
1230
            $this->columns = \func_get_args();
1231
        }
1232
1233
        return $this;
1234
    }
1235
1236
    /**
1237
     * Set VALUES
1238
     * Used in: INSERT, REPLACE
1239
     * func_get_args()-enabled
1240
     *
1241
     * @param array $array The array of values matching the columns from $this->columns()
1242
     *
1243
     * @return self
1244
     */
1245
    public function values($array)
1246
    {
1247
        if (is_array($array)) {
1248
            $this->values[] = $array;
1249
        } else {
1250
            $this->values[] = \func_get_args();
1251
        }
1252
1253
        return $this;
1254
    }
1255
1256
    /**
1257
     * Set column and relative value
1258
     * Used in: INSERT, REPLACE
1259
     *
1260
     * @param string $column The column name
1261
     * @param string $value  The value
1262
     *
1263
     * @return self
1264
     */
1265
    public function value($column, $value)
1266
    {
1267
        if ($this->type === 'insert' || $this->type === 'replace') {
1268
            $this->columns[] = $column;
1269
            $this->values[0][] = $value;
1270
        } else {
1271
            $this->set[$column] = $value;
1272
        }
1273
1274
        return $this;
1275
    }
1276
1277
    /**
1278
     * Allows passing an array with the key as column and value as value
1279
     * Used in: INSERT, REPLACE, UPDATE
1280
     *
1281
     * @param array $array Array of key-values
1282
     *
1283
     * @return self
1284
     */
1285
    public function set($array)
1286
    {
1287
        if ($this->columns === array_keys($array)) {
1288
            $this->values($array);
1289
        } else {
1290
            foreach ($array as $key => $item) {
1291
                $this->value($key, $item);
1292
            }
1293
        }
1294
1295
        return $this;
1296
    }
1297
1298
    /**
1299
     * Allows passing an array with the key as column and value as value
1300
     * Used in: INSERT, REPLACE, UPDATE
1301
     *
1302
     * @param Facet $facet
1303
     *
1304
     * @return self
1305
     */
1306
    public function facet($facet)
1307
    {
1308
        $this->facets[] = $facet;
1309
1310
        return $this;
1311
    }
1312
1313
    /**
1314
     * Sets the characters used for escapeMatch().
1315
     *
1316
     * @param array $array The array of characters to escape
1317
     *
1318
     * @return self
1319
     */
1320
    public function setFullEscapeChars($array = array())
1321
    {
1322
        if (!empty($array)) {
1323
            $this->escape_full_chars = $this->compileEscapeChars($array);
1324
        }
1325
1326
        return $this;
1327
    }
1328
1329
    /**
1330
     * Sets the characters used for halfEscapeMatch().
1331
     *
1332
     * @param array $array The array of characters to escape
1333
     *
1334
     * @return self
1335
     */
1336
    public function setHalfEscapeChars($array = array())
1337
    {
1338
        if (!empty($array)) {
1339
            $this->escape_half_chars = $this->compileEscapeChars($array);
1340
        }
1341
1342
        return $this;
1343
    }
1344
1345
    /**
1346
     * Compiles an array containing the characters and escaped characters into a key/value configuration.
1347
     *
1348
     * @param array $array The array of characters to escape
1349
     *
1350
     * @return array An array of the characters and it's escaped counterpart
1351
     */
1352
    public function compileEscapeChars($array = array())
1353
    {
1354
        $result = array();
1355
        foreach ($array as $character) {
1356
            $result[$character] = '\\'.$character;
1357
        }
1358
1359
        return $result;
1360
    }
1361
1362
    /**
1363
     * Escapes the query for the MATCH() function
1364
     *
1365
     * @param string $string The string to escape for the MATCH
1366
     *
1367
     * @return string The escaped string
1368
     */
1369
    public function escapeMatch($string)
1370
    {
1371
        if (is_null($string)) {
1372
            return '';
1373
        }
1374
1375
        if ($string instanceof Expression) {
1376
            return $string->value();
1377
        }
1378
1379
        return mb_strtolower(str_replace(array_keys($this->escape_full_chars), array_values($this->escape_full_chars), $string), 'utf8');
1380
    }
1381
1382
    /**
1383
     * Escapes the query for the MATCH() function
1384
     * Allows some of the control characters to pass through for use with a search field: -, |, "
1385
     * It also does some tricks to wrap/unwrap within " the string and prevents errors
1386
     *
1387
     * @param string $string The string to escape for the MATCH
1388
     *
1389
     * @return string The escaped string
1390
     */
1391
    public function halfEscapeMatch($string)
1392
    {
1393
        if ($string instanceof Expression) {
1394
            return $string->value();
1395
        }
1396
1397
        $string = str_replace(array_keys($this->escape_half_chars), array_values($this->escape_half_chars), $string);
1398
1399
        // this manages to lower the error rate by a lot
1400
        if (mb_substr_count($string, '"', 'utf8') % 2 !== 0) {
1401
            $string .= '"';
1402
        }
1403
1404
        $string = preg_replace('/-[\s-]*-/u', '-', $string);
1405
1406
        $from_to_preg = array(
1407
            '/([-|])\s*$/u'        => '\\\\\1',
1408
            '/\|[\s|]*\|/u'        => '|',
1409
            '/(\S+)-(\S+)/u'       => '\1\-\2',
1410
            '/(\S+)\s+-\s+(\S+)/u' => '\1 \- \2',
1411
        );
1412
1413
        $string = mb_strtolower(preg_replace(array_keys($from_to_preg), array_values($from_to_preg), $string), 'utf8');
1414
1415
        return $string;
1416
    }
1417
1418
    /**
1419
     * Clears the existing query build for new query when using the same SphinxQL instance.
1420
     *
1421
     * @return self
1422
     */
1423
    public function reset()
1424
    {
1425
        $this->query = null;
1426
        $this->select = array();
1427
        $this->from = array();
1428
        $this->where = array();
1429
        $this->match = array();
1430
        $this->group_by = array();
1431
        $this->group_n_by = null;
1432
        $this->within_group_order_by = array();
1433
        $this->having = array();
1434
        $this->order_by = array();
1435
        $this->offset = null;
1436
        $this->limit = null;
1437
        $this->into = null;
1438
        $this->columns = array();
1439
        $this->values = array();
1440
        $this->set = array();
1441
        $this->options = array();
1442
1443
        return $this;
1444
    }
1445
1446
    /**
1447
     * @return self
1448
     */
1449
    public function resetWhere()
1450
    {
1451
        $this->where = array();
1452
1453
        return $this;
1454
    }
1455
1456
    /**
1457
     * @return self
1458
     */
1459
    public function resetMatch()
1460
    {
1461
        $this->match = array();
1462
1463
        return $this;
1464
    }
1465
1466
    /**
1467
     * @return self
1468
     */
1469
    public function resetGroupBy()
1470
    {
1471
        $this->group_by = array();
1472
        $this->group_n_by = null;
1473
1474
        return $this;
1475
    }
1476
1477
    /**
1478
     * @return self
1479
     */
1480
    public function resetWithinGroupOrderBy()
1481
    {
1482
        $this->within_group_order_by = array();
1483
1484
        return $this;
1485
    }
1486
1487
    /**
1488
     * @return self
1489
     */
1490
    public function resetFacets()
1491
    {
1492
        $this->facets = array();
1493
1494
        return $this;
1495
    }
1496
1497
    /**
1498
     * @return self
1499
     */
1500
    public function resetHaving()
1501
    {
1502
        $this->having = array();
1503
1504
        return $this;
1505
    }
1506
1507
    /**
1508
     * @return self
1509
     */
1510
    public function resetOrderBy()
1511
    {
1512
        $this->order_by = array();
1513
1514
        return $this;
1515
    }
1516
1517
    /**
1518
     * @return self
1519
     */
1520
    public function resetOptions()
1521
    {
1522
        $this->options = array();
1523
1524
        return $this;
1525
    }
1526
}
1527