├── LICENSE ├── README.md └── libraries └── Subquery.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Eric Siegel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Subqueries. For CodeIgniter. 2 | ============================= 3 | **By Eric Siegel** 4 | 5 | 6 | ## Information ## 7 | This is a subquery library for CodeIgniter’s (1.7.x - 2.0.2) active record class. It lets you use the active record methods to create subqueries in your SQL. 8 | 9 | It supports `SELECT`, `JOIN`, `FROM`, `WHERE`, etc. It also supports `UNION ALL`! 10 | 11 | (Yes, you can have subqueries inside subqueries inside `UNION`s and `UNION`s inside subqueries.) 12 | 13 | ## Instructions ## 14 | Put `Subquery.php` into `/application/libraries`, then load it in your code using `$this->load->library('subquery');`. 15 | I guess you can add `'subquery'` to `$autoload['libraries']` (in `/application/config/autoload.php`), if you want. 16 | 17 | ### CodeIgniter 2.1.x ### 18 | This library doesn't work with CodeIgniter 2.1.x out of the box. It requires modifications to a file in `/system` to make it work. 19 | 20 | You need to edit `/system/database/DB_active_rec.php` and modify the signature of `_compile_select` (should be line 1673). 21 | In the older version(s) of CodeIgniter, this function was *not* `protected`, so if you remove the `protected` keyword from the function, my library will work. 22 | 23 | (There's probably a reason this function is `protected`.) 24 | 25 | In the `develop` version of CodeIgniter (which works with this library just fine, by the way), there is a `public` function that you can use. 26 | You can "steal" the `get_compiled_select` function from the `/system/database/DB_query_builder.php` file (line 1283). 27 | 28 | /** 29 | * Get SELECT query string 30 | * 31 | * Compiles a SELECT query string and returns the sql. 32 | * 33 | * @param string the table name to select from (optional) 34 | * @param bool TRUE: resets QB values; FALSE: leave QB vaules alone 35 | * @return string 36 | */ 37 | public function get_compiled_select($table = '', $reset = TRUE) 38 | { 39 | if ($table !== '') 40 | { 41 | $this->_track_aliases($table); 42 | $this->from($table); 43 | } 44 | 45 | $select = $this->_compile_select(); 46 | 47 | if ($reset === TRUE) 48 | { 49 | $this->_reset_select(); 50 | } 51 | 52 | return $select; 53 | } 54 | 55 | Put this function inside `/system/database/DB_active_rec.php`. 56 | 57 | My library will check for the existance of either a `_compile_select` or `get_compiled_select` method. 58 | If none of these methods exist, the library will fail to load. 59 | 60 | ## Methods ## 61 | 62 | - `start_subquery`: Creates a new database object to be used for the subquery 63 | - Parameters: 64 | - `$statement`: SQL statement to put subquery into ('select', 'from', 'join', 'where', 'where_in', etc.) 65 | - `$join_type`: JOIN type (only for join statements) 66 | - `$join_on`: JOIN ON clause (only for join statements) 67 | - Returns: CodeIgniter db object to call active record methods on 68 | - `end_subquery`: Closes the database object and writes the subquery 69 | - Parameters: 70 | - `$alias`: Alias to use in query, or field to use for WHERE 71 | - `$operator`: Operator to use for WHERE ('=', '!=', '<', etc.) / WHERE IN (TRUE for WHERE IN, FALSE for WHERE NOT IN) 72 | - If it's a SELECT, this parameter will turn it into `COALESCE((SELECT ...), $operator) AS $alias` 73 | - `$database`: Database object to use when dbStack is empty (optional) 74 | - Returns: Nothing 75 | - `start_union`: Creates a new database object to be used for unions 76 | - Parameters: None 77 | - Returns: CodeIgniter db object to call active record methods on 78 | - `end_union`: Combines all opened db objects into a `UNION ALL` query 79 | - Parameters: 80 | - `$database`: Database object to use when dbStack is empty (optional) 81 | - Returns: Nothing 82 | 83 | ## Examples ## 84 | 85 | The most basic use of this library is to have a subquery in a `SELECT` statement. This is very simple. 86 | Let's say you want to get this query: 87 | 88 | SELECT field1, (SELECT field2 FROM table2 WHERE table1.field3 = table2.field3) as field2X 89 | FROM table1 WHERE field4 = 'test' 90 | 91 | You would do this in your code: 92 | 93 | $this->db->select('field1'); 94 | $sub = $this->subquery->start_subquery('select'); 95 | $sub->select('field2')->from('table2'); 96 | $sub->where('table1.field3 = table2.field3'); 97 | $this->subquery->end_subquery('field2X'); 98 | $this->db->from('table1') 99 | $this->db->where('field4', 'test'); 100 | 101 | If it's possible that your subquery might return a `NULL` row, you can set a default value. That's done like this: 102 | 103 | $this->db->select('field1'); 104 | $sub = $this->subquery->start_subquery('select'); 105 | $sub->select('field2')->from('table2'); 106 | $sub->where('table1.field3 = table2.field3'); 107 | // Note the second parameter here 108 | $this->subquery->end_subquery('field2X', 'field5'); 109 | $this->db->from('table1') 110 | $this->db->where('field4', 'test'); 111 | 112 | This will generate: 113 | 114 | SELECT field1, COALESCE((SELECT field2 FROM table2 WHERE table1.field3 = table2.field3), field5) as field2X 115 | FROM table1 WHERE field4 = 'test' 116 | 117 | 118 | By passing different values to `start_subquery`, you can make this library do anyting! 119 | 120 | Here's a `WHERE IN` example: 121 | 122 | $this->db->select('field1, field2')->from('table1'); 123 | $sub = $this->subquery->start_subquery('where_in'); 124 | $sub->select('field3')->from('table2')->where('field2', 'test'); 125 | $this->subquery->end_subquery('field4', FALSE); 126 | 127 | This will generate: 128 | 129 | SELECT field1, field2 FROM table1 130 | WHERE field4 NOT IN (SELECT field3 FROM table2 WHERE field2 = 'test') 131 | 132 | `UNION` queries have a *slightly* different syntax. For subqueries, every `start_subquery` needs an `end_subquery`, 133 | but with `UNION` you only need one `end_union` - no matter how many `start_union`s you have. 134 | 135 | $sub1 = $this->subquery->start_union(); 136 | $sub1->select('field1')->from('table1'); 137 | $sub2 = $this->subquery->start_union(); 138 | $sub2->select('field2')->from('table2'); 139 | $sub3 = $this->subquery->start_union(); 140 | $sub3->select('field3')->from('table3'); 141 | $this->subquery->end_union(); 142 | $this->db->order_by('field1', 'DESC'); 143 | 144 | [1]: http://labs.nticompassinc.com 145 | -------------------------------------------------------------------------------- /libraries/Subquery.php: -------------------------------------------------------------------------------- 1 | CI =& get_instance(); 17 | $this->db = $this->CI->db; // Default database connection 18 | 19 | // https://github.com/EllisLab/CodeIgniter/pull/307 20 | $canRun = FALSE; 21 | $functions = array('_compile_select', 'get_compiled_select'); 22 | 23 | foreach($functions as $func){ 24 | if($canRun = is_callable(array($this->db, $func))){ 25 | $this->func = $func; 26 | break; 27 | } 28 | } 29 | 30 | if(!$canRun){ 31 | show_error("Subquery library cannot run. Missing get_compiled_select. Please see the library's documentation."); 32 | } 33 | 34 | $this->prefix = trim($this->db->dbprefix(' ')); 35 | $this->dbStack = array(); 36 | $this->statement = array(); 37 | $this->join_type = array(); 38 | $this->join_on = array(); 39 | $this->unions = 0; 40 | } 41 | 42 | /** 43 | * defaultDB - Sets the default database object to use 44 | * 45 | * @param $database - Database object to use by default 46 | * 47 | */ 48 | function defaultDB($database){ 49 | $this->db = $database; 50 | $this->prefix = trim($this->db->dbprefix(' ')); 51 | } 52 | 53 | /** 54 | * start_subquery - Creates a new database object to be used for the subquery 55 | * 56 | * @param $statement - SQL statement to put subquery into (select, from, join, etc.) 57 | * @param $join_type - JOIN type (only for join statements) 58 | * @param $join_on - JOIN ON clause (only for join statements) 59 | * 60 | * @return A new database object to use for subqueries 61 | */ 62 | function start_subquery($statement, $join_type='', $join_on=1){ 63 | $db = $this->CI->load->database('', true); 64 | if(is_callable(array($db, 'set_dbprefix'))){ 65 | $db->set_dbprefix($this->prefix); 66 | } 67 | 68 | $this->dbStack[] = $db; 69 | $this->statement[] = $statement; 70 | switch(strtolower($statement)){ 71 | case 'join': 72 | case 'join_on': 73 | $this->join_type[] = $join_type; 74 | $this->join_on[] = $join_on; 75 | break; 76 | } 77 | return $db; 78 | } 79 | 80 | /** 81 | * start_union - Creates a new database object to be used for unions 82 | * 83 | * @return A new database object to use for a union query 84 | */ 85 | function start_union(){ 86 | $this->unions++; 87 | return $this->start_subquery(''); 88 | } 89 | 90 | /** 91 | * end_subquery - Closes the database object and writes the subquery 92 | * 93 | * @param $alias - Alias to use in query, or field to use for WHERE 94 | * @param $operator - Operator to use for WHERE (=, !=, <, etc.) / WHERE IN (TRUE for WHERE IN, FALSE for WHERE NOT IN) 95 | * $if_null - If it's a SELECT, this will turn it into COALESCE((SELECT ...), $if_null) AS $alias 96 | * @param $database - Database object to use when dbStack is empty (optional) 97 | * 98 | * @return none 99 | */ 100 | function end_subquery($alias='', $operator=TRUE, $database=FALSE){ 101 | $db = array_pop($this->dbStack); 102 | $sql = '('.$db->{$this->func}().')'; 103 | $db->close(); 104 | $as_alias = $alias!='' ? "AS $alias" : $alias; 105 | $statement = array_pop($this->statement); 106 | if($database === FALSE){ 107 | $database = (count($this->dbStack) == 0) 108 | ? $this->db : $this->dbStack[count($this->dbStack)-1]; 109 | } 110 | $alias = $database->protect_identifiers($alias); 111 | switch(strtolower($statement)){ 112 | case 'join': 113 | $join_type = array_pop($this->join_type); 114 | $join_on = array_pop($this->join_on); 115 | $database->$statement("$sql $as_alias", $join_on, $join_type); 116 | break; 117 | case 'join_on': 118 | $join_type = array_pop($this->join_type); 119 | $join_on = array_pop($this->join_on); 120 | $operator = $operator === TRUE ? '=' : $operator; 121 | // Hack to get around the regex in JOIN 122 | // /([\w\.]+)([\W\s]+)(.+)/ 123 | $sql = preg_replace('/(\n|\r\n)/', ' ', $sql); 124 | $database->join($join_on, "$alias $operator $sql", $join_type); 125 | break; 126 | case 'select': 127 | if($operator !== TRUE){ 128 | $operator = $database->escape($operator); 129 | $database->$statement("COALESCE($sql, $operator) $as_alias", FALSE); 130 | } 131 | else{ 132 | $database->$statement("$sql $as_alias", FALSE); 133 | } 134 | break; 135 | case 'where': 136 | $operator = $operator === TRUE ? '=' : $operator; 137 | $database->where("$alias $operator $sql", NULL, FALSE); 138 | break; 139 | case 'or_where': 140 | $operator = $operator === TRUE ? '=' : $operator; 141 | $database->or_where("$alias $operator $sql", NULL, FALSE); 142 | break; 143 | case 'where_in': 144 | $operator = $operator === TRUE ? 'IN' : 'NOT IN'; 145 | $database->where("$alias $operator $sql", NULL, FALSE); 146 | break; 147 | case 'where_exists': 148 | $operator = $operator === TRUE ? 'EXISTS' : 'NOT EXISTS'; 149 | $database->where("$operator $sql", NULL, FALSE); 150 | break; 151 | case 'from': 152 | $sql = preg_replace_callback('/(GROUP BY (.+?))(?:\s+HAVING .+|\s+ORDER BY .+|\s+LIMIT .+|\)?\s*$)/i', function($matches){ 153 | $groups = preg_split('/,\s?/', $matches[2]); 154 | foreach($groups as &$group){ 155 | $group = preg_replace('/^`?([^`]+)`?(?:\s?(DESC|ASC)?)$/i', '($1) $2', $group); 156 | } 157 | unset($group); 158 | 159 | return 'GROUP BY '.implode(',', $groups).(preg_match('/\)$/', $matches[0]) === 1 ? ')' : ''); 160 | }, $sql); 161 | $database->$statement("$sql $as_alias"); 162 | break; 163 | default: 164 | $database->$statement("$sql $as_alias"); 165 | break; 166 | } 167 | } 168 | 169 | /** 170 | * end_union - Combines all opened db objects into a UNION ALL query 171 | * 172 | * @param $database - Database object to use when dbStack is empty (optional) 173 | * 174 | * @return none 175 | */ 176 | function end_union($database=FALSE){ 177 | $queries = array(); 178 | for($this->unions; $this->unions > 0; $this->unions--){ 179 | $db = array_pop($this->dbStack); 180 | $queries[] = $db->{$this->func}(); 181 | $db->close(); 182 | array_pop($this->statement); 183 | } 184 | $queries = array_reverse($queries); 185 | if(substr($queries[0], 0, 6) == 'SELECT'){ 186 | $queries[0] = substr($queries[0], 7); 187 | } 188 | $sql = implode(' UNION ALL ', $queries); 189 | if($database === FALSE){ 190 | $database = (count($this->dbStack) == 0) 191 | ? $this->db : $this->dbStack[count($this->dbStack)-1]; 192 | } 193 | $database->select($sql, false); 194 | } 195 | 196 | /** 197 | * join_range - Helper function to CROSS JOIN a list of numbers 198 | * From: http://stackoverflow.com/questions/4155873/mysql-find-in-set-vs-in/4156063#4156063 199 | * 200 | * @param $start - Range start 201 | * @param $end - Range end 202 | * @param $alias - Alias for number list 203 | * @param $table_name - JOINed tables need an alias (optional) 204 | * @param $database - Database object to use when dbStack is empty (optional) 205 | */ 206 | function join_range($start, $end, $alias, $table_name='q', $database=FALSE){ 207 | $range = array(); 208 | foreach(range($start, $end) AS $r){ 209 | $range[] = "SELECT $r AS $alias"; 210 | } 211 | $range[0] = substr($range[0], 7); 212 | $range = implode(' UNION ALL ', $range); 213 | 214 | $sub = $this->start_subquery('join', 'inner'); 215 | $sub->select($range, false); 216 | $this->end_subquery($table_name, TRUE, $database); 217 | } 218 | 219 | /** 220 | * dbWrapper - Call a function using "$this->db" in a sandbox, so you don't interfere with other queries 221 | * 222 | * @param $callback - Function to call, only tested with array($obj, 'func') syntax 223 | * @param $params... - Parameters to pass to callback 224 | * 225 | * @return Whatever the callback returns 226 | */ 227 | function dbWrapper($callback){ 228 | $newdb = $this->CI->load->database('', true); 229 | if(is_callable(array($newdb, 'set_dbprefix'))){ 230 | $newdb->set_dbprefix($this->prefix); 231 | } 232 | 233 | $cidb = $this->CI->db; 234 | $this->CI->db = $newdb; 235 | 236 | $params = func_get_args(); 237 | array_shift($params); 238 | $ret = call_user_func_array($callback, $params); 239 | 240 | $this->CI->db = $cidb; 241 | return $ret; 242 | } 243 | } 244 | 245 | ?> 246 | --------------------------------------------------------------------------------