1
|
|
|
<?php defined('SYSPATH') or die('No direct access allowed.'); |
2
|
|
|
/** |
3
|
|
|
* MSSQL Database Driver |
4
|
|
|
* |
5
|
|
|
* @package Core |
6
|
|
|
* @author Kohana Team |
7
|
|
|
* @copyright (c) 2007-2008 Kohana Team |
8
|
|
|
* @license http://kohanaphp.com/license.html |
9
|
|
|
*/ |
10
|
|
|
class Database_Mssql_Driver extends Database_Driver |
11
|
|
|
{ |
12
|
|
|
/** |
13
|
|
|
* Database connection link |
14
|
|
|
*/ |
15
|
|
|
protected $link; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Database configuration |
19
|
|
|
*/ |
20
|
|
|
protected $db_config; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Sets the config for the class. |
24
|
|
|
* |
25
|
|
|
* @param array database configuration |
26
|
|
|
*/ |
27
|
|
|
public function __construct($config) |
28
|
|
|
{ |
29
|
|
|
$this->db_config = $config; |
30
|
|
|
|
31
|
|
|
Kohana::log('debug', 'MSSQL Database Driver Initialized'); |
32
|
|
|
} |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Closes the database connection. |
36
|
|
|
*/ |
37
|
|
|
public function __destruct() |
38
|
|
|
{ |
39
|
|
|
is_resource($this->link) and mssql_close($this->link); |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Make the connection |
44
|
|
|
* |
45
|
|
|
* @return return connection |
46
|
|
|
*/ |
47
|
|
|
public function connect() |
48
|
|
|
{ |
49
|
|
|
// Check if link already exists |
50
|
|
|
if (is_resource($this->link)) { |
51
|
|
|
return $this->link; |
|
|
|
|
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
// Import the connect variables |
55
|
|
|
extract($this->db_config['connection']); |
56
|
|
|
|
57
|
|
|
// Persistent connections enabled? |
58
|
|
|
$connect = ($this->db_config['persistent'] == true) ? 'mssql_pconnect' : 'mssql_connect'; |
59
|
|
|
|
60
|
|
|
// Build the connection info |
61
|
|
|
$host = isset($host) ? $host : $socket; |
62
|
|
|
|
63
|
|
|
// Windows uses a comma instead of a colon |
64
|
|
|
$port = (isset($port) and is_string($port)) ? (KOHANA_IS_WIN ? ',' : ':').$port : ''; |
65
|
|
|
|
66
|
|
|
// Make the connection and select the database |
67
|
|
|
if (($this->link = $connect($host.$port, $user, $pass, true)) and mssql_select_db($database, $this->link)) { |
68
|
|
|
/* This is being removed so I can use it, will need to come up with a more elegant workaround in the future... |
69
|
|
|
* |
70
|
|
|
if ($charset = $this->db_config['character_set']) |
71
|
|
|
{ |
72
|
|
|
$this->set_charset($charset); |
73
|
|
|
} |
74
|
|
|
*/ |
75
|
|
|
|
76
|
|
|
// Clear password after successful connect |
77
|
|
|
$this->db_config['connection']['pass'] = null; |
78
|
|
|
|
79
|
|
|
return $this->link; |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
return false; |
|
|
|
|
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* @param string $sql |
87
|
|
|
*/ |
88
|
|
View Code Duplication |
public function query($sql) |
|
|
|
|
89
|
|
|
{ |
90
|
|
|
// Only cache if it's turned on, and only cache if it's not a write statement |
91
|
|
|
if ($this->db_config['cache'] and ! preg_match('#\b(?:INSERT|UPDATE|REPLACE|SET)\b#i', $sql)) { |
92
|
|
|
$hash = $this->query_hash($sql); |
93
|
|
|
|
94
|
|
|
if (! isset($this->query_cache[$hash])) { |
95
|
|
|
// Set the cached object |
96
|
|
|
$this->query_cache[$hash] = new Mssql_Result(mssql_query($sql, $this->link), $this->link, $this->db_config['object'], $sql); |
97
|
|
|
} else { |
98
|
|
|
// Rewind cached result |
99
|
|
|
$this->query_cache[$hash]->rewind(); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
// Return the cached query |
103
|
|
|
return $this->query_cache[$hash]; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
return new Mssql_Result(mssql_query($sql, $this->link), $this->link, $this->db_config['object'], $sql); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
View Code Duplication |
public function escape_table($table) |
|
|
|
|
110
|
|
|
{ |
111
|
|
|
if (stripos($table, ' AS ') !== false) { |
112
|
|
|
// Force 'AS' to uppercase |
113
|
|
|
$table = str_ireplace(' AS ', ' AS ', $table); |
114
|
|
|
|
115
|
|
|
// Runs escape_table on both sides of an AS statement |
116
|
|
|
$table = array_map(array($this, __FUNCTION__), explode(' AS ', $table)); |
117
|
|
|
|
118
|
|
|
// Re-create the AS statement |
119
|
|
|
return implode(' AS ', $table); |
120
|
|
|
} |
121
|
|
|
return '['.str_replace('.', '[.]', $table).']'; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
View Code Duplication |
public function escape_column($column) |
|
|
|
|
125
|
|
|
{ |
126
|
|
|
if (!$this->db_config['escape']) { |
127
|
|
|
return $column; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
if ($column == '*') { |
131
|
|
|
return $column; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
// This matches any functions we support to SELECT. |
135
|
|
|
if (preg_match('/(avg|count|sum|max|min)\(\s*(.*)\s*\)(\s*as\s*(.+)?)?/i', $column, $matches)) { |
136
|
|
|
if (count($matches) == 3) { |
137
|
|
|
return $matches[1].'('.$this->escape_column($matches[2]).')'; |
138
|
|
|
} elseif (count($matches) == 5) { |
139
|
|
|
return $matches[1].'('.$this->escape_column($matches[2]).') AS '.$this->escape_column($matches[2]); |
140
|
|
|
} |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
// This matches any modifiers we support to SELECT. |
144
|
|
|
if (! preg_match('/\b(?:rand|all|distinct(?:row)?|high_priority|sql_(?:small_result|b(?:ig_result|uffer_result)|no_cache|ca(?:che|lc_found_rows)))\s/i', $column)) { |
145
|
|
|
if (stripos($column, ' AS ') !== false) { |
146
|
|
|
// Force 'AS' to uppercase |
147
|
|
|
$column = str_ireplace(' AS ', ' AS ', $column); |
148
|
|
|
|
149
|
|
|
// Runs escape_column on both sides of an AS statement |
150
|
|
|
$column = array_map(array($this, __FUNCTION__), explode(' AS ', $column)); |
151
|
|
|
|
152
|
|
|
// Re-create the AS statement |
153
|
|
|
return implode(' AS ', $column); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
return preg_replace('/[^.*]+/', '[$0]', $column); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
$parts = explode(' ', $column); |
160
|
|
|
$column = ''; |
161
|
|
|
|
162
|
|
|
for ($i = 0, $c = count($parts); $i < $c; $i++) { |
163
|
|
|
// The column is always last |
164
|
|
|
if ($i == ($c - 1)) { |
165
|
|
|
$column .= preg_replace('/[^.*]+/', '[$0]', $parts[$i]); |
166
|
|
|
} else { // otherwise, it's a modifier |
167
|
|
|
$column .= $parts[$i].' '; |
168
|
|
|
} |
169
|
|
|
} |
170
|
|
|
return $column; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Limit in SQL Server 2000 only uses the keyword |
175
|
|
|
* 'TOP'; 2007 may have an offset keyword, but |
176
|
|
|
* I am unsure - for pagination style limit,offset |
177
|
|
|
* functionality, a fancy query needs to be built. |
178
|
|
|
* |
179
|
|
|
* @param unknown_type $limit |
180
|
|
|
* @return string |
181
|
|
|
*/ |
182
|
|
|
public function limit($limit, $offset=null) |
183
|
|
|
{ |
184
|
|
|
return 'TOP '.$limit; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
View Code Duplication |
public function compile_select($database) |
|
|
|
|
188
|
|
|
{ |
189
|
|
|
$sql = ($database['distinct'] == true) ? 'SELECT DISTINCT ' : 'SELECT '; |
190
|
|
|
$sql .= (count($database['select']) > 0) ? implode(', ', $database['select']) : '*'; |
191
|
|
|
|
192
|
|
|
if (count($database['from']) > 0) { |
193
|
|
|
// Escape the tables |
194
|
|
|
$froms = array(); |
195
|
|
|
foreach ($database['from'] as $from) { |
196
|
|
|
$froms[] = $this->escape_column($from); |
197
|
|
|
} |
198
|
|
|
$sql .= "\nFROM "; |
199
|
|
|
$sql .= implode(', ', $froms); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
if (count($database['join']) > 0) { |
203
|
|
|
foreach ($database['join'] as $join) { |
204
|
|
|
$sql .= "\n".$join['type'].'JOIN '.implode(', ', $join['tables']).' ON '.$join['conditions']; |
205
|
|
|
} |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
if (count($database['where']) > 0) { |
209
|
|
|
$sql .= "\nWHERE "; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
$sql .= implode("\n", $database['where']); |
213
|
|
|
|
214
|
|
|
if (count($database['groupby']) > 0) { |
215
|
|
|
$sql .= "\nGROUP BY "; |
216
|
|
|
$sql .= implode(', ', $database['groupby']); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
if (count($database['having']) > 0) { |
220
|
|
|
$sql .= "\nHAVING "; |
221
|
|
|
$sql .= implode("\n", $database['having']); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
if (count($database['orderby']) > 0) { |
225
|
|
|
$sql .= "\nORDER BY "; |
226
|
|
|
$sql .= implode(', ', $database['orderby']); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
if (is_numeric($database['limit'])) { |
230
|
|
|
$sql .= "\n"; |
231
|
|
|
$sql .= $this->limit($database['limit']); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
return $sql; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
public function escape_str($str) |
238
|
|
|
{ |
239
|
|
|
if (!$this->db_config['escape']) { |
240
|
|
|
return $str; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
is_resource($this->link) or $this->connect(); |
244
|
|
|
//mssql_real_escape_string($str, $this->link); <-- this function doesn't exist |
245
|
|
|
|
246
|
|
|
$characters = array('/\x00/', '/\x1a/', '/\n/', '/\r/', '/\\\/', '/\'/'); |
247
|
|
|
$replace = array('\\\x00', '\\x1a', '\\n', '\\r', '\\\\', "''"); |
248
|
|
|
return preg_replace($characters, $replace, $str); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
public function list_tables() |
252
|
|
|
{ |
253
|
|
|
$sql = 'SHOW TABLES FROM ['.$this->db_config['connection']['database'].']'; |
254
|
|
|
$result = $this->query($sql)->result(false, MSSQL_ASSOC); |
|
|
|
|
255
|
|
|
|
256
|
|
|
$retval = array(); |
257
|
|
|
foreach ($result as $row) { |
258
|
|
|
$retval[] = current($row); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
return $retval; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
public function show_error() |
265
|
|
|
{ |
266
|
|
|
return mssql_get_last_message($this->link); |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
public function list_fields($table) |
270
|
|
|
{ |
271
|
|
|
$result = array(); |
272
|
|
|
|
273
|
|
|
foreach ($this->field_data($table) as $row) { |
274
|
|
|
// Make an associative array |
275
|
|
|
$result[$row->Field] = $this->sql_type($row->Type); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
return $result; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
public function field_data($table) |
282
|
|
|
{ |
283
|
|
|
$query = $this->query("SELECT COLUMN_NAME AS Field, DATA_TYPE as Type FROM INFORMATION_SCHEMA.Columns WHERE TABLE_NAME = '".$this->escape_table($table)."'", $this->link); |
|
|
|
|
284
|
|
|
|
285
|
|
|
return $query->result_array(true); |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* MSSQL Result |
291
|
|
|
*/ |
292
|
|
|
class Mssql_Result extends Database_Result |
293
|
|
|
{ |
294
|
|
|
|
295
|
|
|
// Fetch function and return type |
296
|
|
|
protected $fetch_type = 'mssql_fetch_object'; |
297
|
|
|
protected $return_type = MSSQL_ASSOC; |
298
|
|
|
|
299
|
|
|
/** |
300
|
|
|
* Sets up the result variables. |
301
|
|
|
* |
302
|
|
|
* @param resource query result |
303
|
|
|
* @param resource database link |
304
|
|
|
* @param boolean return objects or arrays |
305
|
|
|
* @param string SQL query that was run |
306
|
|
|
*/ |
307
|
|
|
public function __construct($result, $link, $object = true, $sql) |
308
|
|
|
{ |
309
|
|
|
$this->result = $result; |
310
|
|
|
|
311
|
|
|
// If the query is a resource, it was a SELECT, SHOW, DESCRIBE, EXPLAIN query |
312
|
|
|
if (is_resource($result)) { |
313
|
|
|
$this->current_row = 0; |
314
|
|
|
$this->total_rows = mssql_num_rows($this->result); |
315
|
|
|
$this->fetch_type = ($object === true) ? 'mssql_fetch_object' : 'mssql_fetch_array'; |
316
|
|
|
} elseif (is_bool($result)) { |
317
|
|
|
if ($result == false) { |
|
|
|
|
318
|
|
|
// SQL error |
319
|
|
|
throw new Kohana_Database_Exception('database.error', mssql_get_last_message($link).' - '.$sql); |
320
|
|
|
} else { |
321
|
|
|
// Its an DELETE, INSERT, REPLACE, or UPDATE querys |
322
|
|
|
$last_id = mssql_query('SELECT @@IDENTITY AS last_id', $link); |
323
|
|
|
$result = mssql_fetch_assoc($last_id); |
324
|
|
|
$this->insert_id = $result['last_id']; |
325
|
|
|
$this->total_rows = mssql_rows_affected($link); |
326
|
|
|
} |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
// Set result type |
330
|
|
|
$this->result($object); |
331
|
|
|
|
332
|
|
|
// Store the SQL |
333
|
|
|
$this->sql = $sql; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* Destruct, the cleanup crew! |
338
|
|
|
*/ |
339
|
|
|
public function __destruct() |
340
|
|
|
{ |
341
|
|
|
if (is_resource($this->result)) { |
342
|
|
|
mssql_free_result($this->result); |
343
|
|
|
} |
344
|
|
|
} |
345
|
|
|
|
346
|
|
View Code Duplication |
public function result($object = true, $type = MSSQL_ASSOC) |
|
|
|
|
347
|
|
|
{ |
348
|
|
|
$this->fetch_type = ((bool) $object) ? 'mssql_fetch_object' : 'mssql_fetch_array'; |
349
|
|
|
|
350
|
|
|
// This check has to be outside the previous statement, because we do not |
351
|
|
|
// know the state of fetch_type when $object = NULL |
352
|
|
|
// NOTE - The class set by $type must be defined before fetching the result, |
353
|
|
|
// autoloading is disabled to save a lot of stupid overhead. |
354
|
|
|
if ($this->fetch_type == 'mssql_fetch_object') { |
355
|
|
|
$this->return_type = (is_string($type) and Kohana::auto_load($type)) ? $type : 'stdClass'; |
356
|
|
|
} else { |
357
|
|
|
$this->return_type = $type; |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
return $this; |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
public function as_array($object = null, $type = MSSQL_ASSOC) |
364
|
|
|
{ |
365
|
|
|
return $this->result_array($object, $type); |
366
|
|
|
} |
367
|
|
|
|
368
|
|
View Code Duplication |
public function result_array($object = null, $type = MSSQL_ASSOC) |
|
|
|
|
369
|
|
|
{ |
370
|
|
|
$rows = array(); |
371
|
|
|
|
372
|
|
|
if (is_string($object)) { |
373
|
|
|
$fetch = $object; |
374
|
|
|
} elseif (is_bool($object)) { |
375
|
|
|
if ($object === true) { |
376
|
|
|
$fetch = 'mssql_fetch_object'; |
377
|
|
|
|
378
|
|
|
// NOTE - The class set by $type must be defined before fetching the result, |
379
|
|
|
// autoloading is disabled to save a lot of stupid overhead. |
380
|
|
|
$type = (is_string($type) and Kohana::auto_load($type)) ? $type : 'stdClass'; |
381
|
|
|
} else { |
382
|
|
|
$fetch = 'mssql_fetch_array'; |
383
|
|
|
} |
384
|
|
|
} else { |
385
|
|
|
// Use the default config values |
386
|
|
|
$fetch = $this->fetch_type; |
387
|
|
|
|
388
|
|
|
if ($fetch == 'mssql_fetch_object') { |
389
|
|
|
$type = (is_string($type) and Kohana::auto_load($type)) ? $type : 'stdClass'; |
390
|
|
|
} |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
if (mssql_num_rows($this->result)) { |
394
|
|
|
// Reset the pointer location to make sure things work properly |
395
|
|
|
mssql_data_seek($this->result, 0); |
396
|
|
|
|
397
|
|
|
while ($row = $fetch($this->result, $type)) { |
398
|
|
|
$rows[] = $row; |
399
|
|
|
} |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
return isset($rows) ? $rows : array(); |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
public function list_fields() |
406
|
|
|
{ |
407
|
|
|
$field_names = array(); |
408
|
|
|
while ($field = mssql_fetch_field($this->result)) { |
409
|
|
|
$field_names[] = $field->name; |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
return $field_names; |
413
|
|
|
} |
414
|
|
|
|
415
|
|
|
public function seek($offset) |
416
|
|
|
{ |
417
|
|
|
if (! $this->offsetExists($offset)) { |
418
|
|
|
return false; |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
return mssql_data_seek($this->result, $offset); |
422
|
|
|
} |
423
|
|
|
} // End mssql_Result Class |
424
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.