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
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\sphinx;
use yii\base\InvalidParamException;
use yii\base\NotSupportedException;
use yii\base\Object;
use yii\db\Exception;
use yii\db\Expression;
/**
* QueryBuilder builds a SELECT SQL statement based on the specification given as a [[Query]] object.
*
* QueryBuilder can also be used to build SQL statements such as INSERT, REPLACE, UPDATE, DELETE,
* from a [[Query]] object.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0
*/
class QueryBuilder extends Object
{
/**
* The prefix for automatically generated query binding parameters.
*/
const PARAM_PREFIX = ':qp';
/**
* @var Connection the Sphinx connection.
*/
public $db;
/**
* @var string the separator between different fragments of a SQL statement.
* Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement.
*/
public $separator = " ";
/**
* @var array map of query condition to builder methods.
* These methods are used by [[buildCondition]] to build SQL conditions from array syntax.
*/
protected $conditionBuilders = [
'AND' => 'buildAndCondition',
'OR' => 'buildAndCondition',
'BETWEEN' => 'buildBetweenCondition',
'NOT BETWEEN' => 'buildBetweenCondition',
'IN' => 'buildInCondition',
'NOT IN' => 'buildInCondition',
'LIKE' => 'buildLikeCondition',
'NOT LIKE' => 'buildLikeCondition',
'OR LIKE' => 'buildLikeCondition',
'OR NOT LIKE' => 'buildLikeCondition',
'NOT' => 'buildNotCondition',
];
/**
* Constructor.
* @param Connection $connection the Sphinx connection.
* @param array $config name-value pairs that will be used to initialize the object properties
*/
public function __construct($connection, $config = [])
{
$this->db = $connection;
parent::__construct($config);
}
/**
* Generates a SELECT SQL statement from a [[Query]] object.
* @param Query $query the [[Query]] object from which the SQL statement will be generated
* @param array $params the parameters to be bound to the generated SQL statement. These parameters will
* be included in the result with the additional parameters generated during the query building process.
* @throws NotSupportedException if query contains 'join' option.
* @return array the generated SQL statement (the first array element) and the corresponding
* parameters to be bound to the SQL statement (the second array element). The parameters returned
* include those provided in `$params`.
*/
public function build($query, $params = [])
{
$query = $query->prepare($this);
if (!empty($query->join)) {
throw new NotSupportedException('Build of "' . get_class($query) . '::join" is not supported.');
}
$params = empty($params) ? $query->params : array_merge($params, $query->params);
$from = $query->from;
if ($from === null && $query instanceof ActiveQuery) {
/* @var $modelClass ActiveRecord */
$modelClass = $query->modelClass;
$from = [$modelClass::indexName()];
}
$clauses = [
$this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
$this->buildFrom($from, $params),
$this->buildWhere($query->from, $query->where, $params, $query->match),
$this->buildGroupBy($query->groupBy),
$this->buildWithin($query->within),
$this->buildHaving($query->from, $query->having, $params),
$this->buildOrderBy($query->orderBy),
$this->buildLimit($query->limit, $query->offset),
$this->buildOption($query->options, $params),
];
return [implode($this->separator, array_filter($clauses)), $params];
}
/**
* Creates an INSERT SQL statement.
* For example,
*
* ~~~
* $sql = $queryBuilder->insert('idx_user', [
* 'name' => 'Sam',
* 'age' => 30,
* 'id' => 10,
* ], $params);
* ~~~
*
* The method will properly escape the index and column names.
*
* @param string $index the index that new rows will be inserted into.
* @param array $columns the column data (name => value) to be inserted into the index.
* @param array $params the binding parameters that will be generated by this method.
* They should be bound to the Sphinx command later.
* @return string the INSERT SQL
*/
public function insert($index, $columns, &$params)
{
return $this->generateInsertReplace('INSERT', $index, $columns, $params);
}
/**
* Creates an REPLACE SQL statement.
* For example,
*
* ~~~
* $sql = $queryBuilder->replace('idx_user', [
* 'name' => 'Sam',
* 'age' => 30,
* 'id' => 10,
* ], $params);
* ~~~
*
* The method will properly escape the index and column names.
*
* @param string $index the index that new rows will be replaced.
* @param array $columns the column data (name => value) to be replaced in the index.
* @param array $params the binding parameters that will be generated by this method.
* They should be bound to the Sphinx command later.
* @return string the INSERT SQL
*/
public function replace($index, $columns, &$params)
{
return $this->generateInsertReplace('REPLACE', $index, $columns, $params);
}
/**
* Generates INSERT/REPLACE SQL statement.
* @param string $statement statement ot be generated.
* @param string $index the affected index name.
* @param array $columns the column data (name => value).
* @param array $params the binding parameters that will be generated by this method.
* @return string generated SQL
*/
protected function generateInsertReplace($statement, $index, $columns, &$params)
{
if (($indexSchema = $this->db->getIndexSchema($index)) !== null) {
$indexSchemas = [$indexSchema];
} else {
$indexSchemas = [];
}
$names = [];
$placeholders = [];
foreach ($columns as $name => $value) {
$names[] = $this->db->quoteColumnName($name);
$placeholders[] = $this->composeColumnValue($indexSchemas, $name, $value, $params);
}
return $statement . ' INTO ' . $this->db->quoteIndexName($index)
. ' (' . implode(', ', $names) . ') VALUES ('
. implode(', ', $placeholders) . ')';
}
/**
* Generates a batch INSERT SQL statement.
* For example,
*
* ~~~
* $sql = $queryBuilder->batchInsert('idx_user', ['id', 'name', 'age'], [
* [1, 'Tom', 30],
* [2, 'Jane', 20],
* [3, 'Linda', 25],
* ], $params);
* ~~~
*
* Note that the values in each row must match the corresponding column names.
*
* @param string $index the index that new rows will be inserted into.
* @param array $columns the column names
* @param array $rows the rows to be batch inserted into the index
* @param array $params the binding parameters that will be generated by this method.
* They should be bound to the Sphinx command later.
* @return string the batch INSERT SQL statement
*/
public function batchInsert($index, $columns, $rows, &$params)
{
return $this->generateBatchInsertReplace('INSERT', $index, $columns, $rows, $params);
}
/**
* Generates a batch REPLACE SQL statement.
* For example,
*
* ~~~
* $sql = $queryBuilder->batchReplace('idx_user', ['id', 'name', 'age'], [
* [1, 'Tom', 30],
* [2, 'Jane', 20],
* [3, 'Linda', 25],
* ], $params);
* ~~~
*
* Note that the values in each row must match the corresponding column names.
*
* @param string $index the index that new rows will be replaced.
* @param array $columns the column names
* @param array $rows the rows to be batch replaced in the index
* @param array $params the binding parameters that will be generated by this method.
* They should be bound to the Sphinx command later.
* @return string the batch INSERT SQL statement
*/
public function batchReplace($index, $columns, $rows, &$params)
{
return $this->generateBatchInsertReplace('REPLACE', $index, $columns, $rows, $params);
}
/**
* Generates a batch INSERT/REPLACE SQL statement.
* @param string $statement statement ot be generated.
* @param string $index the affected index name.
* @param array $columns the column data (name => value).
* @param array $rows the rows to be batch inserted into the index
* @param array $params the binding parameters that will be generated by this method.
* @return string generated SQL
*/
protected function generateBatchInsertReplace($statement, $index, $columns, $rows, &$params)
{
if (($indexSchema = $this->db->getIndexSchema($index)) !== null) {
$indexSchemas = [$indexSchema];
} else {
$indexSchemas = [];
}
foreach ($columns as $i => $name) {
$columns[$i] = $this->db->quoteColumnName($name);
}
$values = [];
foreach ($rows as $row) {
$vs = [];
foreach ($row as $i => $value) {
$vs[] = $this->composeColumnValue($indexSchemas, $columns[$i], $value, $params);
}
$values[] = '(' . implode(', ', $vs) . ')';
}
return $statement . ' INTO ' . $this->db->quoteIndexName($index)
. ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
}
/**
* Creates an UPDATE SQL statement.
* For example,
*
* ~~~
* $params = [];
* $sql = $queryBuilder->update('idx_user', ['status' => 1], 'age > 30', $params);
* ~~~
*
* The method will properly escape the index and column names.
*
* @param string $index the index to be updated.
* @param array $columns the column data (name => value) to be updated.
* @param array|string $condition the condition that will be put in the WHERE part. Please
* refer to [[Query::where()]] on how to specify condition.
* @param array $params the binding parameters that will be modified by this method
* so that they can be bound to the Sphinx command later.
* @param array $options list of options in format: optionName => optionValue
* @return string the UPDATE SQL
*/
public function update($index, $columns, $condition, &$params, $options)
{
if (($indexSchema = $this->db->getIndexSchema($index)) !== null) {
$indexSchemas = [$indexSchema];
} else {
$indexSchemas = [];
}
$lines = [];
foreach ($columns as $name => $value) {
$lines[] = $this->db->quoteColumnName($name) . '=' . $this->composeColumnValue($indexSchemas, $name, $value, $params);
}
$sql = 'UPDATE ' . $this->db->quoteIndexName($index) . ' SET ' . implode(', ', $lines);
$where = $this->buildWhere([$index], $condition, $params);
if ($where !== '') {
$sql = $sql . ' ' . $where;
}
$option = $this->buildOption($options, $params);
if ($option !== '') {
$sql = $sql . ' ' . $option;
}
return $sql;
}
/**
* Creates a DELETE SQL statement.
* For example,
*
* ~~~
* $sql = $queryBuilder->delete('idx_user', 'status = 0');
* ~~~
*
* The method will properly escape the index and column names.
*
* @param string $index the index where the data will be deleted from.
* @param array|string $condition the condition that will be put in the WHERE part. Please
* refer to [[Query::where()]] on how to specify condition.
* @param array $params the binding parameters that will be modified by this method
* so that they can be bound to the Sphinx command later.
* @return string the DELETE SQL
*/
public function delete($index, $condition, &$params)
{
$sql = 'DELETE FROM ' . $this->db->quoteIndexName($index);
$where = $this->buildWhere([$index], $condition, $params);
return $where === '' ? $sql : $sql . ' ' . $where;
}
/**
* Builds a SQL statement for truncating an index.
* @param string $index the index to be truncated. The name will be properly quoted by the method.
* @return string the SQL statement for truncating an index.
*/
public function truncateIndex($index)
{
return 'TRUNCATE RTINDEX ' . $this->db->quoteIndexName($index);
}
/**
* Builds a SQL statement for call snippet from provided data and query, using specified index settings.
* @param string $index name of the index, from which to take the text processing settings.
* @param string|array $source is the source data to extract a snippet from.
* It could be either a single string or array of strings.
* @param string $match the full-text query to build snippets for.
* @param array $options list of options in format: optionName => optionValue
* @param array $params the binding parameters that will be modified by this method
* so that they can be bound to the Sphinx command later.
* @return string the SQL statement for call snippets.
*/
public function callSnippets($index, $source, $match, $options, &$params)
{
if (is_array($source)) {
$dataSqlParts = [];
foreach ($source as $sourceRow) {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $sourceRow;
$dataSqlParts[] = $phName;
}
$dataSql = '(' . implode(',', $dataSqlParts) . ')';
} else {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $source;
$dataSql = $phName;
}
$indexParamName = self::PARAM_PREFIX . count($params);
$params[$indexParamName] = $index;
$matchParamName = self::PARAM_PREFIX . count($params);
$params[$matchParamName] = $match;
if (!empty($options)) {
$optionParts = [];
foreach ($options as $name => $value) {
if ($value instanceof Expression) {
$actualValue = $value->expression;
} else {
$actualValue = self::PARAM_PREFIX . count($params);
$params[$actualValue] = $value;
}
$optionParts[] = $actualValue . ' AS ' . $name;
}
$optionSql = ', ' . implode(', ', $optionParts);
} else {
$optionSql = '';
}
return 'CALL SNIPPETS(' . $dataSql. ', ' . $indexParamName . ', ' . $matchParamName . $optionSql. ')';
}
/**
* Builds a SQL statement for returning tokenized and normalized forms of the keywords, and,
* optionally, keyword statistics.
* @param string $index the name of the index from which to take the text processing settings
* @param string $text the text to break down to keywords.
* @param boolean $fetchStatistic whether to return document and hit occurrence statistics
* @param array $params the binding parameters that will be modified by this method
* so that they can be bound to the Sphinx command later.
* @return string the SQL statement for call keywords.
*/
public function callKeywords($index, $text, $fetchStatistic, &$params)
{
$indexParamName = self::PARAM_PREFIX . count($params);
$params[$indexParamName] = $index;
$textParamName = self::PARAM_PREFIX . count($params);
$params[$textParamName] = $text;
return 'CALL KEYWORDS(' . $textParamName . ', ' . $indexParamName . ($fetchStatistic ? ', 1' : '') . ')';
}
/**
* @param array $columns
* @param array $params the binding parameters to be populated
* @param boolean $distinct
* @param string $selectOption
* @return string the SELECT clause built from [[query]].
*/
public function buildSelect($columns, &$params, $distinct = false, $selectOption = null)
{
$select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
if ($selectOption !== null) {
$select .= ' ' . $selectOption;
}
if (empty($columns)) {
return $select . ' *';
}
foreach ($columns as $i => $column) {
if ($column instanceof Expression) {
$columns[$i] = $column->expression;
$params = array_merge($params, $column->params);
} elseif (is_string($i)) {
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
$columns[$i] = "$column AS " . $this->db->quoteColumnName($i);
} elseif (strpos($column, '(') === false) {
if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) {
$columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]);
} else {
$columns[$i] = $this->db->quoteColumnName($column);
}
}
}
return $select . ' ' . implode(', ', $columns);
}
/**
* @param array $indexes
* @param array $params the binding parameters to be populated
* @return string the FROM clause built from [[query]].
*/
public function buildFrom($indexes, &$params)
{
if (empty($indexes)) {
return '';
}
foreach ($indexes as $i => $index) {
if ($index instanceof Query) {
list($sql, $params) = $this->build($index, $params);
$indexes[$i] = "($sql) " . $this->db->quoteIndexName($i);
} elseif (is_string($i)) {
if (strpos($index, '(') === false) {
$index = $this->db->quoteIndexName($index);
}
$indexes[$i] = "$index " . $this->db->quoteIndexName($i);
} elseif (strpos($index, '(') === false) {
if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $index, $matches)) { // with alias
$indexes[$i] = $this->db->quoteIndexName($matches[1]) . ' ' . $this->db->quoteIndexName($matches[2]);
} else {
$indexes[$i] = $this->db->quoteIndexName($index);
}
}
}
if (is_array($indexes)) {
$indexes = implode(', ', $indexes);
}
return 'FROM ' . $indexes;
}
/**
* @param string[] $indexes list of index names, which affected by query
* @param string|array $condition
* @param array $params the binding parameters to be populated
* @param string|Expression|null $match
* @return string the WHERE clause built from [[query]].
*/
public function buildWhere($indexes, $condition, &$params, $match = null)
{
if ($match !== null) {
if ($match instanceof Expression) {
$matchWhere = 'MATCH(' . $match->expression . ')';
$params = array_merge($params, $match->params);
} else {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $this->db->escapeMatchValue($match);
$matchWhere = 'MATCH(' . $phName . ')';
}
if ($condition === null) {
$condition = $matchWhere;
} else {
$condition = ['and', $matchWhere, $condition];
}
}
if (empty($condition)) {
return '';
}
$indexSchemas = $this->getIndexSchemas($indexes);
$where = $this->buildCondition($indexSchemas, $condition, $params);
return $where === '' ? '' : 'WHERE ' . $where;
}
/**
* @param array $columns
* @return string the GROUP BY clause
*/
public function buildGroupBy($columns)
{
return empty($columns) ? '' : 'GROUP BY ' . $this->buildColumns($columns);
}
/**
* @param string[] $indexes list of index names, which affected by query
* @param string|array $condition
* @param array $params the binding parameters to be populated
* @return string the HAVING clause built from [[Query::$having]].
*/
public function buildHaving($indexes, $condition, &$params)
{
if (empty($condition)) {
return '';
}
$indexSchemas = $this->getIndexSchemas($indexes);
$having = $this->buildCondition($indexSchemas, $condition, $params);
return $having === '' ? '' : 'HAVING ' . $having;
}
/**
* Builds the ORDER BY and LIMIT/OFFSET clauses and appends them to the given SQL.
* @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET)
* @param array $orderBy the order by columns. See [[Query::orderBy]] for more details on how to specify this parameter.
* @param integer $limit the limit number. See [[Query::limit]] for more details.
* @param integer $offset the offset number. See [[Query::offset]] for more details.
* @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any)
*/
public function buildOrderByAndLimit($sql, $orderBy, $limit, $offset)
{
$orderBy = $this->buildOrderBy($orderBy);
if ($orderBy !== '') {
$sql .= $this->separator . $orderBy;
}
$limit = $this->buildLimit($limit, $offset);
if ($limit !== '') {
$sql .= $this->separator . $limit;
}
return $sql;
}
/**
* @param array $columns
* @return string the ORDER BY clause built from [[query]].
*/
public function buildOrderBy($columns)
{
if (empty($columns)) {
return '';
}
$orders = [];
foreach ($columns as $name => $direction) {
if ($direction instanceof Expression) {
$orders[] = $direction->expression;
} else {
$orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : 'ASC');
}
}
return 'ORDER BY ' . implode(', ', $orders);
}
/**
* @param integer $limit
* @param integer $offset
* @return string the LIMIT and OFFSET clauses built from [[query]].
*/
public function buildLimit($limit, $offset)
{
$sql = '';
if (is_integer($offset) && $offset > 0 || is_string($offset) && ctype_digit($offset) && $offset !== '0') {
$sql = 'LIMIT ' . $offset;
}
if (is_string($limit) && ctype_digit($limit) || is_integer($limit) && $limit >= 0) {
$sql = $sql === '' ? "LIMIT $limit" : "$sql,$limit";
} elseif ($sql !== '') {
$sql .= ',1000'; // this is the default limit by sphinx
}
return $sql;
}
/**
* Processes columns and properly quote them if necessary.
* It will join all columns into a string with comma as separators.
* @param string|array $columns the columns to be processed
* @return string the processing result
*/
public function buildColumns($columns)
{
if (!is_array($columns)) {
if (strpos($columns, '(') !== false) {
return $columns;
} else {
$columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
}
}
foreach ($columns as $i => $column) {
if ($column instanceof Expression) {
$columns[$i] = $column->expression;
} elseif (strpos($column, '(') === false) {
$columns[$i] = $this->db->quoteColumnName($column);
}
}
return is_array($columns) ? implode(', ', $columns) : $columns;
}
/**
* Parses the condition specification and generates the corresponding SQL expression.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string|array $condition the condition specification. Please refer to [[Query::where()]]
* on how to specify a condition.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws \yii\db\Exception if the condition is in bad format
*/
public function buildCondition($indexes, $condition, &$params)
{
if (!is_array($condition)) {
return (string) $condition;
} elseif (empty($condition)) {
return '';
}
if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
$operator = strtoupper($condition[0]);
if (isset($this->conditionBuilders[$operator])) {
$method = $this->conditionBuilders[$operator];
} else {
$method = 'buildSimpleCondition';
}
array_shift($condition);
return $this->$method($indexes, $operator, $condition, $params);
} else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
return $this->buildHashCondition($indexes, $condition, $params);
}
}
/**
* Creates a condition based on column-value pairs.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param array $condition the condition specification.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
*/
public function buildHashCondition($indexes, $condition, &$params)
{
$parts = [];
foreach ($condition as $column => $value) {
if (is_array($value) || $value instanceof Query) {
// IN condition
$parts[] = $this->buildInCondition($indexes, 'IN', [$column, $value], $params);
} else {
if (strpos($column, '(') === false) {
$quotedColumn = $this->db->quoteColumnName($column);
} else {
$quotedColumn = $column;
}
if ($value === null) {
$parts[] = "$quotedColumn IS NULL";
} else {
$parts[] = $quotedColumn . '=' . $this->composeColumnValue($indexes, $column, $value, $params);
}
}
}
return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')';
}
/**
* Connects two or more SQL expressions with the `AND` or `OR` operator.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string $operator the operator to use for connecting the given operands
* @param array $operands the SQL expressions to connect.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
*/
public function buildAndCondition($indexes, $operator, $operands, &$params)
{
$parts = [];
foreach ($operands as $operand) {
if (is_array($operand)) {
$operand = $this->buildCondition($indexes, $operand, $params);
}
if ($operand !== '') {
$parts[] = $operand;
}
}
if (!empty($parts)) {
return '(' . implode(") $operator (", $parts) . ')';
} else {
return '';
}
}
/**
* Inverts an SQL expressions with `NOT` operator.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string $operator the operator to use for connecting the given operands
* @param array $operands the SQL expressions to connect.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws InvalidParamException if wrong number of operands have been given.
*/
public function buildNotCondition($indexes, $operator, $operands, &$params)
{
if (count($operands) != 1) {
throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
}
$operand = reset($operands);
if (is_array($operand)) {
$operand = $this->buildCondition($indexes, $operand, $params);
}
if ($operand === '') {
return '';
}
return "$operator ($operand)";
}
/**
* Creates an SQL expressions with the `BETWEEN` operator.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`)
* @param array $operands the first operand is the column name. The second and third operands
* describe the interval that column value should be in.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws Exception if wrong number of operands have been given.
*/
public function buildBetweenCondition($indexes, $operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1], $operands[2])) {
throw new Exception("Operator '$operator' requires three operands.");
}
list($column, $value1, $value2) = $operands;
if (strpos($column, '(') === false) {
$quotedColumn = $this->db->quoteColumnName($column);
} else {
$quotedColumn = $column;
}
$phName1 = $this->composeColumnValue($indexes, $column, $value1, $params);
$phName2 = $this->composeColumnValue($indexes, $column, $value2, $params);
return "$quotedColumn $operator $phName1 AND $phName2";
}
/**
* Creates an SQL expressions with the `IN` operator.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
* @param array $operands the first operand is the column name. If it is an array
* a composite IN condition will be generated.
* The second operand is an array of values that column value should be among.
* If it is an empty array the generated expression will be a `false` value if
* operator is `IN` and empty if operator is `NOT IN`.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws Exception if wrong number of operands have been given.
*/
public function buildInCondition($indexes, $operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1])) {
throw new Exception("Operator '$operator' requires two operands.");
}
list($column, $values) = $operands;
if ($values === [] || $column === []) {
return $operator === 'IN' ? '0=1' : '';
}
if ($values instanceof Query) {
// sub-query
list($sql, $params) = $this->build($values, $params);
$column = (array)$column;
if (is_array($column)) {
foreach ($column as $i => $col) {
if (strpos($col, '(') === false) {
$column[$i] = $this->db->quoteColumnName($col);
}
}
return '(' . implode(', ', $column) . ") $operator ($sql)";
} else {
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
return "$column $operator ($sql)";
}
}
$values = (array) $values;
if (count($column) > 1) {
return $this->buildCompositeInCondition($indexes, $operator, $column, $values, $params);
}
if (is_array($column)) {
$column = reset($column);
}
foreach ($values as $i => $value) {
if (is_array($value)) {
$value = isset($value[$column]) ? $value[$column] : null;
}
$values[$i] = $this->composeColumnValue($indexes, $column, $value, $params);
}
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
if (count($values) > 1) {
return "$column $operator (" . implode(', ', $values) . ')';
} else {
$operator = $operator === 'IN' ? '=' : '<>';
return $column . $operator . reset($values);
}
}
/**
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
* @param array $columns
* @param array $values
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
*/
protected function buildCompositeInCondition($indexes, $operator, $columns, $values, &$params)
{
$vss = [];
foreach ($values as $value) {
$vs = [];
foreach ($columns as $column) {
if (isset($value[$column])) {
$vs[] = $this->composeColumnValue($indexes, $column, $value[$column], $params);
} else {
$vs[] = 'NULL';
}
}
$vss[] = '(' . implode(', ', $vs) . ')';
}
foreach ($columns as $i => $column) {
if (strpos($column, '(') === false) {
$columns[$i] = $this->db->quoteColumnName($column);
}
}
return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')';
}
/**
* Creates an SQL expressions with the `LIKE` operator.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
* @param array $operands an array of two or three operands
*
* - The first operand is the column name.
* - The second operand is a single value or an array of values that column value
* should be compared with. If it is an empty array the generated expression will
* be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator
* is `NOT LIKE` or `OR NOT LIKE`.
* - An optional third operand can also be provided to specify how to escape special characters
* in the value(s). The operand should be an array of mappings from the special characters to their
* escaped counterparts. If this operand is not provided, a default escape mapping will be used.
* You may use `false` or an empty array to indicate the values are already escaped and no escape
* should be applied. Note that when using an escape mapping (or the third operand is not provided),
* the values will be automatically enclosed within a pair of percentage characters.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws InvalidParamException if wrong number of operands have been given.
*/
public function buildLikeCondition($indexes, $operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1])) {
throw new InvalidParamException("Operator '$operator' requires two operands.");
}
$escape = isset($operands[2]) ? $operands[2] : ['%'=>'\%', '_'=>'\_', '\\'=>'\\\\'];
unset($operands[2]);
list($column, $values) = $operands;
$values = (array) $values;
if (empty($values)) {
return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : '';
}
if ($operator === 'LIKE' || $operator === 'NOT LIKE') {
$andor = ' AND ';
} else {
$andor = ' OR ';
$operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE';
}
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
$parts = [];
foreach ($values as $value) {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%');
$parts[] = "$column $operator $phName";
}
return implode($andor, $parts);
}
/**
* @param array $columns
* @return string the ORDER BY clause built from [[query]].
*/
public function buildWithin($columns)
{
if (empty($columns)) {
return '';
}
$orders = [];
foreach ($columns as $name => $direction) {
if ($direction instanceof Expression) {
$orders[] = $direction->expression;
} else {
$orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
}
}
return 'WITHIN GROUP ORDER BY ' . implode(', ', $orders);
}
/**
* @param array $options query options in format: optionName => optionValue
* @param array $params the binding parameters to be populated
* @return string the OPTION clause build from [[query]]
*/
public function buildOption($options, &$params)
{
if (empty($options)) {
return '';
}
$optionLines = [];
foreach ($options as $name => $value) {
if ($value instanceof Expression) {
$actualValue = $value->expression;
} else {
if (is_array($value)) {
$actualValueParts = [];
foreach ($value as $key => $valuePart) {
if (is_numeric($key)) {
$actualValuePart = '';
} else {
$actualValuePart = $key . ' = ';
}
if ($valuePart instanceof Expression) {
$actualValuePart .= $valuePart->expression;
} else {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $valuePart;
$actualValuePart .= $phName;
}
$actualValueParts[] = $actualValuePart;
}
$actualValue = '(' . implode(', ', $actualValueParts) . ')';
} else {
$actualValue = self::PARAM_PREFIX . count($params);
$params[$actualValue] = $value;
}
}
$optionLines[] = $name . ' = ' . $actualValue;
}
return 'OPTION ' . implode(', ', $optionLines);
}
/**
* Composes column value for SQL, taking in account the column type.
* @param IndexSchema[] $indexes list of indexes, which affected by query
* @param string $columnName name of the column
* @param mixed $value raw column value
* @param array $params the binding parameters to be populated
* @return string SQL expression, which represents column value
*/
protected function composeColumnValue($indexes, $columnName, $value, &$params)
{
if ($value === null) {
return 'NULL';
} elseif ($value instanceof Expression) {
$params = array_merge($params, $value->params);
return $value->expression;
}
foreach ($indexes as $index) {
$columnSchema = $index->getColumn($columnName);
if ($columnSchema !== null) {
break;
}
}
if (is_array($value)) {
// MVA :
$lineParts = [];
foreach ($value as $subValue) {
if ($subValue instanceof Expression) {
$params = array_merge($params, $subValue->params);
$lineParts[] = $subValue->expression;
} else {
$phName = self::PARAM_PREFIX . count($params);
$lineParts[] = $phName;
$params[$phName] = (isset($columnSchema)) ? $columnSchema->dbTypecast($subValue) : $subValue;
}
}
return '(' . implode(',', $lineParts) . ')';
} else {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = (isset($columnSchema)) ? $columnSchema->dbTypecast($value) : $value;
return $phName;
}
}
/**
* Creates an SQL expressions like `"column" operator value`.
* @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc.
* @param array $operands contains two column names.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws InvalidParamException if count($operands) is not 2
*/
public function buildSimpleCondition($operator, $operands, &$params)
{
if (count($operands) !== 2) {
throw new InvalidParamException("Operator '$operator' requires two operands.");
}
list($column, $value) = $operands;
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
if ($value === null) {
return "$column $operator NULL";
} else {
$phName = self::PARAM_PREFIX . count($params);
$params[$phName] = $value;
return "$column $operator $phName";
}
}
/**
* @param array $indexes index names.
* @return IndexSchema[] index schemas.
*/
private function getIndexSchemas($indexes)
{
$indexSchemas = [];
if (!empty($indexes)) {
foreach ($indexes as $indexName) {
$index = $this->db->getIndexSchema($indexName);
if ($index !== null) {
$indexSchemas[] = $index;
}
}
}
return $indexSchemas;
}
}