├── 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 |
--------------------------------------------------------------------------------