From ce4ce91a7ee33160e54e724d9d8e029de67313bb Mon Sep 17 00:00:00 2001 From: Rexhep Shijaku <35240528+rexshijaku@users.noreply.github.com> Date: Thu, 3 Jun 2021 22:24:16 +0200 Subject: [PATCH] 1. 'Date/year/month/day/time' functions were added. 2. 'Advanced Joins' implemented ( which involves the solution for the #2 issue ) 3. Licence file added 4. Some work on Aliased Tables (on from and joins) --- LICENSE | 21 ++++++++ examples/data_time.php | 50 ++++++++++++++++++ examples/join.php | 24 ++++++++- src/Options.php | 2 + src/builders/CriterionBuilder.php | 7 +++ src/builders/JoinBuilder.php | 51 +++++++++++++++--- src/extractors/AbstractExtractor.php | 10 ++-- src/extractors/CriterionExtractor.php | 76 +++++++++++++++++++++++---- src/extractors/FromExtractor.php | 9 ++-- src/extractors/JoinExtractor.php | 42 ++++++--------- src/utils/CriterionContext.php | 1 + src/utils/CriterionTypes.php | 1 + 12 files changed, 240 insertions(+), 54 deletions(-) create mode 100644 LICENSE create mode 100644 examples/data_time.php diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1ae566e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Rexhep Shijaku + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/data_time.php b/examples/data_time.php new file mode 100644 index 0000000..2a15cbc --- /dev/null +++ b/examples/data_time.php @@ -0,0 +1,50 @@ +convert($sql); +// prints +// DB::table('members') +// ->whereDate('created_at', '=', '2021-03-31') +// ->get(); + +//========================================================== + +$sql = 'SELECT * FROM members WHERE YEAR(created_at) = 1991'; +echo $converter->convert($sql); +// prints +// DB::table('members') +// ->whereYear('created_at', '=', 1991) +// ->get(); + +//========================================================== + +$sql = 'SELECT * FROM members WHERE MONTH(created_at) = 12 '; +echo $converter->convert($sql); +// DB::table('members') +// ->whereMonth('created_at', '=', 12) +// ->get(); + +//========================================================== + +$sql = 'SELECT * FROM members WHERE DAY(created_at) = 15 '; +echo $converter->convert($sql); +// prints +// DB::table('members') +// ->whereDay('created_at', '=', 15) +// ->get(); + +//========================================================== + +$sql = 'SELECT * FROM members WHERE TIME(created_at) = "11:20:45" '; +echo $converter->convert($sql); +// prints +// DB::table('members') +// ->whereTime('created_at', '=', '11:20:45') +// ->get(); diff --git a/examples/join.php b/examples/join.php index 07ed541..e2ecaaa 100644 --- a/examples/join.php +++ b/examples/join.php @@ -43,4 +43,26 @@ // ->crossJoin('details') // ->get(); -//========================================================== \ No newline at end of file +//===================Advanced Join Clauses======================== + +$sql = 'SELECT * FROM members JOIN details + ON members.id = details.members_id + AND age > 10 AND age NOT BETWEEN 10 AND 20 + AND title IS NOT NULL AND NOT age > 10 AND NAME LIKE "%Jo%" + AND age NOT IN (10,20,30) + LEFT JOIN further_details fd + ON details.id = fd.details_id'; +echo $converter->convert($sql); +// prints +// DB::table('members') +// ->join('details', function ($join) { +// $join->on('members.id', '=', 'details.members_id') +// ->where('age', '>', 10) +// ->whereNotBetween('age', [10, 20]) +// ->whereNotNull('title') +// ->whereRaw(' NOT age > ? ', [10]) +// ->where('NAME', 'LIKE', '%Jo%') +// ->whereNotIn('age', [10, 20, 30]); +// }) +// ->leftJoin(DB::raw('further_details fd'), 'details.id', '=', 'fd.details_id') +// ->get(); \ No newline at end of file diff --git a/src/Options.php b/src/Options.php index 60ee84e..7672e96 100644 --- a/src/Options.php +++ b/src/Options.php @@ -10,6 +10,7 @@ class Options 'facade' => 'DB::', 'group' => true ); + private $supporting_fn = array('date', 'month', 'year' ,'day', 'time'); public function __construct($options) { @@ -31,6 +32,7 @@ public function set(): void unset($this->options['settings']); // unset reserved $this->options['settings']['agg'] = $this->aggregate_fn; + $this->options['settings']['fns'] = $this->supporting_fn; } public function get() diff --git a/src/builders/CriterionBuilder.php b/src/builders/CriterionBuilder.php index 08c150f..6418729 100644 --- a/src/builders/CriterionBuilder.php +++ b/src/builders/CriterionBuilder.php @@ -104,6 +104,13 @@ public function build(array $parts, array &$skip_bag = array()) $fn = $this->getValue($part['sep']) == 'or' ? 'orWhereRaw' : 'whereRaw'; $query_val .= '->' . $fn . '(' . $this->quote($part['field'] . ' AGAINST ' . $part['value']) . ')'; break; + case CriterionTypes::Function: + $fn = $this->getValue($part['sep']) == 'or' ? 'orWhere' : 'where'; + $fn = $this->fnMerger(array($fn, $part['fn'])); + $op = $part['operator']; + $inner = $this->quote($part['field']) . ',' . $this->quote(strtoupper($op)) . ',' . $this->wrapValue($part['value']['value']); + $query_val .= '->' . $fn . '(' . $inner . ')'; + break; default: break; } diff --git a/src/builders/JoinBuilder.php b/src/builders/JoinBuilder.php index ae42693..ec86408 100644 --- a/src/builders/JoinBuilder.php +++ b/src/builders/JoinBuilder.php @@ -22,17 +22,56 @@ public function build(array $parts, array &$skip_bag = array()) foreach ($parts as $join) { - $condition = implode('', $join['condition_separators']); if ($this->getValue($join['type']) !== 'join') { // left,right,cross etc $fn = $this->fnMerger(array(strtolower($join['type']), 'join')); } else $fn = $this->fnMerger(array('join')); - $qb .= "->" . $fn . "(" . $this->quote($join['table']); - if (!empty($join['condition_fields'])) { // in cross e.g are empty - $qb .= "," . $this->quote($join['condition_fields'][0]) - . "," . $this->quote($condition) - . "," . $this->quote($join['condition_fields'][1]); + $qb .= "->" . $fn . "(" . $this->buildRawable($join['table'], $join['table_is_raw']); + if (isset($join['on_clause']) && count($join['on_clause']) > 0) // in cross join no on_clause! + { + // everything except columns are raw ! + if (count($join['on_clause']) == 1 + && $join['on_clause'][0]['type'] !== 'between' + && $join['on_clause'][0]['raw_field'] === false + && $join['on_clause'][0]['raw_value'] === false) { + + $on_clause = $join['on_clause'][0]; + $qb .= "," . $this->quote($on_clause['field']) + . "," . $this->quote(implode(' ', $on_clause['operators'])) + . "," . $this->quote($on_clause['value']); + } else { + + $qb .= ',' . 'function($join) {'; + $qb .= '$join'; + + foreach ($join['on_clause'] as $on_clause) { + + if ($on_clause['type'] == 'between' || $on_clause['raw_field'] || $on_clause['raw_value']) { + if (isset($on_clause['const_value'])) + $on_clause['raw_value'] = !$on_clause['const_value']; + $builder = new CriterionBuilder($this->options); + $q = $builder->build(array($on_clause)); + $qb .= $q; + } else { + // no raw found and not between + $operators = implode(' ', $on_clause['operators']); + $fn_parts = $on_clause['sep'] == 'and' ? array('on') : array('or', 'on'); + + $qb .= '->'; + $qb .= $this->fnMerger($fn_parts); + $qb .= '('; + + $qb .= $this->quote($on_clause['field'], $on_clause['raw_field']) + . "," . $this->quote($operators) + . "," . $this->quote($on_clause['value'], + !$on_clause['const_value'] && $on_clause['raw_value']); + + $qb .= ')'; + } + } + $qb .= '; }'; + } } $qb .= ")"; } diff --git a/src/extractors/AbstractExtractor.php b/src/extractors/AbstractExtractor.php index f0a9b55..5bd0865 100644 --- a/src/extractors/AbstractExtractor.php +++ b/src/extractors/AbstractExtractor.php @@ -119,7 +119,7 @@ function mergeExpressionParts($parts) return (implode('', $parts)); } - protected function getWithAlias($val) + protected function getWithAlias($val, &$is_raw) { if ($val['expr_type'] === 'table') $return = $val['table']; // no alias here, if any, it will be added at the end @@ -130,8 +130,12 @@ protected function getWithAlias($val) $return = $val['base_expr']; } } - if ($this->hasAlias($val)) - $return .= ' ' . $val['alias']['base_expr']; + if ($this->hasAlias($val)) { + $return .= ' '; + if ($val['alias']['as'] === false) // because Laravel escapes 'table t' expressions entirely! + $is_raw = true; + $return .= $val['alias']['base_expr']; + } return $return; } diff --git a/src/extractors/CriterionExtractor.php b/src/extractors/CriterionExtractor.php index f2e38bc..f63ad45 100644 --- a/src/extractors/CriterionExtractor.php +++ b/src/extractors/CriterionExtractor.php @@ -81,7 +81,8 @@ function getCriteriaParts($value, &$parts = array(), $context = CriterionContext 'value' => $res_value['value'], 'raw_field' => $res_field['is_raw'], 'raw_value' => $res_value['is_raw'], - 'sep' => $logical_operator + 'sep' => $logical_operator, + 'const_value' => $res_value['is_const'] ); break; case 'is': @@ -89,7 +90,7 @@ function getCriteriaParts($value, &$parts = array(), $context = CriterionContext $this->handle_outer_negation = true; $res_field = $this->getLeft($index, $value); - $res_value = $this->getRight($index, $value, $curr_index); + $res_value = $this->getRight($index, $value, $curr_index, $context); $operators_ = array('is'); if ($res_value['has_negation']) @@ -102,7 +103,9 @@ function getCriteriaParts($value, &$parts = array(), $context = CriterionContext 'value' => $res_value['value'], 'raw_field' => $res_field['is_raw'], 'raw_value' => $res_value['is_raw'], - 'sep' => $logical_operator); // now combine fields + operators + 'sep' => $logical_operator, + 'const_value' => $res_value['is_const'] + ); // now combine fields + operators break; case "between": $btw_operators = array(); @@ -131,7 +134,7 @@ function getCriteriaParts($value, &$parts = array(), $context = CriterionContext $like_operators[] = 'like'; $res_field = $this->getLeft($index, $value); - $res_val = $this->getRight($index, $value, $curr_index); + $res_val = $this->getRight($index, $value, $curr_index, $context); $parts[] = array( @@ -141,7 +144,8 @@ function getCriteriaParts($value, &$parts = array(), $context = CriterionContext 'value' => $res_val['value'], 'raw_field' => $res_field['is_raw'], 'raw_value' => $res_val['is_raw'], - 'sep' => $logical_operator); + 'sep' => $logical_operator, + 'const_value' => $res_val['is_const']); break; case "in": @@ -161,8 +165,8 @@ function getCriteriaParts($value, &$parts = array(), $context = CriterionContext 'raw_field' => $res_field['is_raw'], 'raw_value' => $res_val['is_raw'], 'sep' => $logical_operator, - 'as_php_arr' => $res_val['value_type'] == 'in-list' - ); + 'as_php_arr' => $res_val['value_type'] == 'in-list', + 'const_value' => $res_val['is_const']); break; case "not": @@ -196,6 +200,37 @@ function getCriteriaParts($value, &$parts = array(), $context = CriterionContext 'sep' => $logical_operator ); + } else if (CriterionContext::Where == $context) { + $fn = $this->getValue($val['base_expr']); + + if (in_array($fn, $this->options['settings']['fns'])) { + + if($val['sub_tree'] !== false && $this->isRaw($val['sub_tree'][0])) + continue; + + $params = ''; // params is field in this context + $this->getFnParams($val, $params); + + $temp_index = $curr_index; + $curr_index = $index = ($index + 1); // move to operator + $sep = $this->getValue($value[$curr_index]['base_expr']); + $res_val = $this->getRight($index, $value, $curr_index, $context); + + if ($res_val['is_raw']) { + $curr_index = $index = $temp_index; + continue; + } + + + $parts[] = array( + 'type' => CriterionTypes::Function, + 'fn' => $fn, + 'field' => $params, + 'value' => $res_val, + 'operator' => $sep, + 'sep' => $logical_operator + ); + } } } @@ -307,6 +342,8 @@ function getRight($index, $value, &$curr_index, $context = CriterionContext::Whe $right_operator = ''; $is_raw = false; + $is_const = null; + while (!$this->isLogicalOperator($right_operator)) { // x > 2 and (until you find first logical operator keep looping) $right_ind++; if ($right_ind < count($value)) { @@ -317,7 +354,10 @@ function getRight($index, $value, &$curr_index, $context = CriterionContext::Whe $right_operator = $this->getValue($value[$right_ind]['base_expr']); else { $value_ .= $value[$right_ind]['base_expr']; - $is_raw = true; // if some operation is happening then the expression should not be escaped + if ($context === CriterionContext::Join) // because on x=x+5, x+5 is escaped entirely ! + $is_const = false; + else + $is_raw = true; // if some operation is happening then the expression should not be escaped } if ($right_operator == 'not') @@ -329,8 +369,21 @@ function getRight($index, $value, &$curr_index, $context = CriterionContext::Whe break; } else { $value_type = $value[$right_ind]['expr_type']; - if ($value[$right_ind]['expr_type'] != 'const') - $is_raw = true; + if ($context === CriterionContext::Join) { // on x = y (both x,y must be column) + if ($value[$right_ind]['expr_type'] != 'colref') + $is_raw = true; + + if (!isset($is_const)) { + if ($value[$right_ind]['expr_type'] == 'const') + $is_const = true; + else + $is_const = false; + } + + } else { + if ($value[$right_ind]['expr_type'] != 'const') + $is_raw = true; + } if ($value_type == 'subquery') $value_type = 'field_only'; @@ -342,7 +395,8 @@ function getRight($index, $value, &$curr_index, $context = CriterionContext::Whe break; } $curr_index = $right_ind; - return array('value' => $value_, 'has_negation' => $has_negation, 'is_raw' => $is_raw, 'value_type' => $value_type); + return array('value' => $value_, 'has_negation' => $has_negation, + 'is_raw' => $is_raw, 'value_type' => $value_type, 'is_const' => $is_const); } private function getBetweenValue($index, $value, &$curr_index) diff --git a/src/extractors/FromExtractor.php b/src/extractors/FromExtractor.php index 9ccf03b..a50cc2e 100644 --- a/src/extractors/FromExtractor.php +++ b/src/extractors/FromExtractor.php @@ -34,12 +34,9 @@ public function extract(array $value, array $parsed = array()) function extractSingle($value) { - return array('table' => $this->getTable($value), 'is_raw' => $value[0]['expr_type'] != 'table'); - } - - private function getTable($value) - { - return $this->getWithAlias($value[0]); + $is_raw = $value[0]['expr_type'] != 'table'; + $table = $this->getWithAlias($value[0], $is_raw); + return array('table' => $table, 'is_raw' => $is_raw); } } \ No newline at end of file diff --git a/src/extractors/JoinExtractor.php b/src/extractors/JoinExtractor.php index cf7d25a..aeee039 100644 --- a/src/extractors/JoinExtractor.php +++ b/src/extractors/JoinExtractor.php @@ -2,6 +2,7 @@ namespace RexShijaku\SQLToLaravelBuilder\extractors; +use RexShijaku\SQLToLaravelBuilder\utils\CriterionContext; /** * This class extracts and compiles SQL query parts for the following Query Builder methods : * @@ -26,41 +27,28 @@ public function extract(array $value, array $parsed = array()) if (!$this->validJoin($val['join_type'])) // skip joins such as natural continue; - $join_table = $this->getTableVal($val); + $is_raw_table = false; + $join_table = $this->getWithAlias($val, $is_raw_table); - $separators = array(); - $condition_value = array(); - if ($val['ref_clause'] !== false) { - foreach ($val['ref_clause'] as $r) { - if ($r['expr_type'] == 'operator') - $separators[] = $r['base_expr']; - else - $condition_value[] = $r['base_expr']; - } - } - - $join = array('table' => $join_table, - 'condition_fields' => $condition_value, - 'condition_separators' => $separators, + $join = array( + 'table' => $join_table, + 'table_is_raw' => $is_raw_table, 'type' => $val['join_type'] ); + + if ($val['ref_clause'] !== false) + $join['on_clause'] = $this->getOnCriterion($val['ref_clause']); + $joins[] = $join; } return $joins; } - private function getTableVal($val) + private function getOnCriterion($val) { - if ($val['expr_type'] == 'table') - $return = $val['table']; - else { - if ($val['expr_type'] == 'subquery') { - $return = '(' . $val['base_expr'] . ')'; - } else - $return = $val['base_expr']; - } - if ($this->hasAlias($val)) - $return .= ' ' . $val['alias']['base_expr']; - return $return; + $parts = array(); + $criterion = new CriterionExtractor($this->options); + $criterion->getCriteriaParts($val, $parts, CriterionContext::Join); + return $parts; } } \ No newline at end of file diff --git a/src/utils/CriterionContext.php b/src/utils/CriterionContext.php index b92453e..7012ba0 100644 --- a/src/utils/CriterionContext.php +++ b/src/utils/CriterionContext.php @@ -6,4 +6,5 @@ class CriterionContext { const Having = 'having'; const Where = 'where'; + const Join = 'join'; } \ No newline at end of file diff --git a/src/utils/CriterionTypes.php b/src/utils/CriterionTypes.php index cdd6cbf..b49f692 100644 --- a/src/utils/CriterionTypes.php +++ b/src/utils/CriterionTypes.php @@ -12,4 +12,5 @@ class CriterionTypes const InFieldValue = 'in_field_value'; const Is = 'is'; const Like = 'like'; + const Function = 'fn'; } \ No newline at end of file