1
|
|
|
<?php |
2
|
|
|
namespace Fwlib\Bridge; |
3
|
|
|
|
4
|
|
|
use Fwlib\Db\SqlGenerator; |
5
|
|
|
use Fwlib\Util\UtilContainerAwareTrait; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Extended ADOdb class |
9
|
|
|
* |
10
|
|
|
* Diff with ADOdb official method of extend sub-class by db type, neither |
11
|
|
|
* direct extend ADOdb class, here use ADOdb instance as property only, and |
12
|
|
|
* use magic function __call __get __set to route ADOdb method to it. The |
13
|
|
|
* opposite this way is no modification to RecordSet class. |
14
|
|
|
* |
15
|
|
|
* |
16
|
|
|
* Notice: |
17
|
|
|
* |
18
|
|
|
* ADOdb for sybase under Windows 2003, call Affected_Rows() will cause |
19
|
|
|
* process error. |
20
|
|
|
* |
21
|
|
|
* |
22
|
|
|
* Encoding convert: |
23
|
|
|
* |
24
|
|
|
* If charset of database and system are different, this class will try to do |
25
|
|
|
* encoding convert before query, see __call() for affected method. |
26
|
|
|
* |
27
|
|
|
* Encoding convert for query result will NOT automatic done, although we |
28
|
|
|
* provide a method convertEncodingResult() to do this manually. |
29
|
|
|
* |
30
|
|
|
* @copyright Copyright 2008-2015, 2017 Fwolf |
31
|
|
|
* @license http://www.gnu.org/licenses/lgpl.html LGPL-3.0+ |
32
|
|
|
*/ |
33
|
|
|
class Adodb |
34
|
|
|
{ |
35
|
|
|
use UtilContainerAwareTrait; |
36
|
|
|
|
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* PHP script charset |
40
|
|
|
* |
41
|
|
|
* Used to compare with db charset. |
42
|
|
|
* |
43
|
|
|
* This is charset of PHP script. Operation system has their default |
44
|
|
|
* charset, but text editor can specify other charset too. |
45
|
|
|
* |
46
|
|
|
* @var string |
47
|
|
|
*/ |
48
|
|
|
public $charsetPhp = 'UTF-8'; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Real ADOdb connection object |
52
|
|
|
* |
53
|
|
|
* @var object |
54
|
|
|
*/ |
55
|
|
|
protected $conn = null; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* Table schema |
59
|
|
|
* |
60
|
|
|
* { |
61
|
|
|
* table: { |
62
|
|
|
* col: { |
63
|
|
|
* name: ts |
64
|
|
|
* max_length: -1 |
65
|
|
|
* type: timestamp |
66
|
|
|
* scale: |
67
|
|
|
* not_null: |
68
|
|
|
* primary_key: |
69
|
|
|
* auto_increment: |
70
|
|
|
* binary: |
71
|
|
|
* unsigned: |
72
|
|
|
* zerofill: |
73
|
|
|
* has_default: 1 |
74
|
|
|
* default_value: CURRENT_TIMESTAMP |
75
|
|
|
* } |
76
|
|
|
* } |
77
|
|
|
* } |
78
|
|
|
* |
79
|
|
|
* Notice: col is ADOFieldObject object, not array ! |
80
|
|
|
* |
81
|
|
|
* @var array |
82
|
|
|
*/ |
83
|
|
|
public $metaColumn = []; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Table column name array, with upper case column name as index |
87
|
|
|
* |
88
|
|
|
* { |
89
|
|
|
* COLUMN: column |
90
|
|
|
* } |
91
|
|
|
* |
92
|
|
|
* @var array |
93
|
|
|
*/ |
94
|
|
|
public $metaColumnName = []; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Primary key columns of table |
98
|
|
|
* |
99
|
|
|
* { |
100
|
|
|
* tbl: colPk |
101
|
|
|
* OR |
102
|
|
|
* tbl: [pkCol1, pkCol2] |
103
|
|
|
* } |
104
|
|
|
* |
105
|
|
|
* @var array |
106
|
|
|
*/ |
107
|
|
|
public $metaPrimaryKey = []; |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* Db profile |
111
|
|
|
* |
112
|
|
|
* {host, user, pass, name, type, lang} |
113
|
|
|
* |
114
|
|
|
* @var array |
115
|
|
|
*/ |
116
|
|
|
public $profile = null; |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Total query count |
120
|
|
|
* |
121
|
|
|
* @var int |
122
|
|
|
*/ |
123
|
|
|
protected $queryCount = 0; |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Sql generator object |
127
|
|
|
* |
128
|
|
|
* @var SqlGenerator |
129
|
|
|
*/ |
130
|
|
|
protected $sqlGenerator; |
131
|
|
|
|
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* constructor |
135
|
|
|
* |
136
|
|
|
* $dbprofile = {type:, host:, user:, pass:, name:, lang:,} |
137
|
|
|
* type: mysql/sybase_ase etc. |
138
|
|
|
* name: db name to select. |
139
|
|
|
* lang: db server charset. |
140
|
|
|
* |
141
|
|
|
* if $pathAdodb is empty, should load ADOdb through ClassLoader. |
142
|
|
|
* |
143
|
|
|
* @param array $profile |
144
|
|
|
* @param string $pathAdodb Include path of original ADOdb |
145
|
|
|
*/ |
146
|
|
|
public function __construct($profile, $pathAdodb = '') |
147
|
|
|
{ |
148
|
|
|
// @codeCoverageIgnoreStart |
149
|
|
|
// Include ADOdb lib |
150
|
|
|
if (!empty($pathAdodb)) { |
151
|
|
|
/** @noinspection PhpIncludeInspection */ |
152
|
|
|
require_once $pathAdodb; |
153
|
|
|
} |
154
|
|
|
// @codeCoverageIgnoreEnd |
155
|
|
|
|
156
|
|
|
// Trigger AutoLoader for ADOdb |
157
|
|
|
new \ADOFetchObj; |
158
|
|
|
|
159
|
|
|
$this->profile = $profile; |
160
|
|
|
$this->conn = ADONewConnection($profile['type']); |
161
|
|
|
|
162
|
|
|
// From ADOdb 5.11 Execute 2d array is disabled by default, we need |
163
|
|
|
// enable it for using write etc. |
164
|
|
|
$this->conn->bulkBind = true; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Redirect method call to ADOdb |
170
|
|
|
* |
171
|
|
|
* @var string $name Method name |
172
|
|
|
* @var array $arg Method argument |
173
|
|
|
* @return mixed |
174
|
|
|
*/ |
175
|
|
|
public function __call($name, $arg) |
176
|
|
|
{ |
177
|
|
|
// Before call, convert $sql encoding first |
178
|
|
|
// Method list by ADOdb doc order |
179
|
|
|
|
180
|
|
|
if (in_array( |
181
|
|
|
$name, |
182
|
|
|
[ |
183
|
|
|
'Execute', |
184
|
|
|
'SelectLimit', |
185
|
|
|
'Prepare', |
186
|
|
|
'PrepareSP', |
187
|
|
|
'GetOne', |
188
|
|
|
'GetRow', |
189
|
|
|
'GetAll', |
190
|
|
|
'GetCol', |
191
|
|
|
'GetAssoc', |
192
|
|
|
'ExecuteCursor', |
193
|
|
|
] |
194
|
|
|
)) { |
195
|
|
|
// $sql is the 1st param |
196
|
|
|
$this->convertEncodingSql($arg[0]); |
197
|
|
|
} elseif (in_array( |
198
|
|
|
$name, |
199
|
|
|
[ |
200
|
|
|
'CacheExecute', |
201
|
|
|
'CacheSelectLimit', |
202
|
|
|
'CacheGetOne', |
203
|
|
|
'CacheGetRow', |
204
|
|
|
'CacheGetAll', |
205
|
|
|
'CacheGetCol', |
206
|
|
|
'CacheGetAssoc', |
207
|
|
|
] |
208
|
|
|
)) { |
209
|
|
|
// $sql is the 2nd param |
210
|
|
|
$this->convertEncodingSql($arg[1]); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
// Count db query times, for all Adodb instance. |
214
|
|
|
// Use static var so multi Adodb object can be included in count. |
215
|
|
|
// Use standalone func for easy extend by sub class. |
216
|
|
|
// CacheXxx() method is not counted. |
217
|
|
View Code Duplication |
if (in_array( |
|
|
|
|
218
|
|
|
$name, |
219
|
|
|
[ |
220
|
|
|
'Execute', |
221
|
|
|
'SelectLimit', |
222
|
|
|
'GetOne', |
223
|
|
|
'GetRow', |
224
|
|
|
'GetAll', |
225
|
|
|
'GetCol', |
226
|
|
|
'GetAssoc', |
227
|
|
|
'ExecuteCursor', |
228
|
|
|
] |
229
|
|
|
)) { |
230
|
|
|
$this->countQuery(); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
return call_user_func_array([$this->conn, $name], $arg); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Redirect property get to ADOdb |
239
|
|
|
* |
240
|
|
|
* @param string $name |
241
|
|
|
* @return mixed |
242
|
|
|
*/ |
243
|
|
|
public function __get($name) |
244
|
|
|
{ |
245
|
|
|
return $this->conn->$name; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Redirect property set to adodb |
251
|
|
|
* |
252
|
|
|
* @param string $name |
253
|
|
|
* @param mixed $val |
254
|
|
|
*/ |
255
|
|
|
public function __set($name, $val) |
256
|
|
|
{ |
257
|
|
|
// For object need auto new in this class instead of $this->conn, |
258
|
|
|
// with mechanism in class AbstractAutoNewInstance with newInstanceXxx() |
259
|
|
|
// method, need check in __get() and __set() both. If only treat in |
260
|
|
|
// __get(), the new instance and assign operate will happen, but its |
261
|
|
|
// assigned to $this->conn->property, instead of $this->property, next |
262
|
|
|
// time when it's used(get), will trigger __get() again, and do useless |
263
|
|
|
// newInstanceXxx() again. |
264
|
|
|
// |
265
|
|
|
// By use get method similar with getService(), this is not problem |
266
|
|
|
// anymore. |
267
|
|
|
|
268
|
|
|
$this->conn->$name = $val; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Connect to db |
274
|
|
|
* |
275
|
|
|
* If db is mysql, will auto execute 'set names utf8'. |
276
|
|
|
* |
277
|
|
|
* @see $profile |
278
|
|
|
* @param boolean $forcenew Force new connection |
279
|
|
|
* @return boolean |
280
|
|
|
*/ |
281
|
|
|
public function connect($forcenew = false) |
282
|
|
|
{ |
283
|
|
|
if (!$forcenew && $this->isConnected()) { |
284
|
|
|
return true; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
|
288
|
|
|
// @codeCoverageIgnoreStart |
289
|
|
|
// Mysqli doesn't allow port in host, grab it out and set |
290
|
|
View Code Duplication |
if ('mysqli' == strtolower($this->conn->databaseType)) { |
|
|
|
|
291
|
|
|
$ar = []; |
292
|
|
|
$i = preg_match('/:(\d+)$/', $this->profile['host'], $ar); |
293
|
|
|
if (0 < $i) { |
294
|
|
|
$this->conn->port = $ar[1]; |
295
|
|
|
$this->profile['host'] = |
296
|
|
|
preg_replace('/:(\d+)$/', '', $this->profile['host']); |
297
|
|
|
} |
298
|
|
|
} |
299
|
|
|
// @codeCoverageIgnoreEnd |
300
|
|
|
|
301
|
|
|
|
302
|
|
|
// To eliminate sybase 'Changed database context to XXX' message, |
303
|
|
|
// should edit php.ini and change mssql.min_message_severity to 11. |
304
|
|
|
// @link https://bugs.php.net/bug.php?id=34784 |
305
|
|
|
|
306
|
|
|
|
307
|
|
|
try { |
308
|
|
|
// Disable error display temporary |
309
|
|
|
$iniDisplayErrors = ini_get('display_errors'); |
310
|
|
|
ini_set('display_errors', '0'); |
311
|
|
|
|
312
|
|
|
$rs = $this->conn->Connect( |
313
|
|
|
$this->profile['host'], |
314
|
|
|
$this->profile['user'], |
315
|
|
|
$this->profile['pass'], |
316
|
|
|
$this->profile['name'] |
317
|
|
|
); |
318
|
|
|
|
319
|
|
|
// Recover original error display setting |
320
|
|
|
ini_set('display_errors', $iniDisplayErrors); |
321
|
|
|
|
322
|
|
|
if (empty($rs)) { |
323
|
|
|
// @codeCoverageIgnoreStart |
324
|
|
|
throw new \Exception($this->conn->ErrorMsg(), -1); |
325
|
|
|
// @codeCoverageIgnoreEnd |
326
|
|
|
} |
327
|
|
|
} catch (\Exception $e) { |
328
|
|
|
// @codeCoverageIgnoreStart |
329
|
|
|
// Log and output error |
330
|
|
|
$trace = "======== Adodb Connect Error ========\n" |
331
|
|
|
//. $e->getTraceAsString() . "\n" |
332
|
|
|
. $e->getMessage() . "\n"; |
333
|
|
|
error_log($trace); |
334
|
|
|
|
335
|
|
|
if (!$this->getUtilContainer()->getEnv()->isCli()) { |
336
|
|
|
$trace = $this->getUtilContainer()->getString() |
337
|
|
|
->encodeHtml($trace); |
338
|
|
|
} |
339
|
|
|
echo $trace; |
340
|
|
|
|
341
|
|
|
return false; |
342
|
|
|
// @codeCoverageIgnoreEnd |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
// @codeCoverageIgnoreStart |
346
|
|
|
// Mysql db need to 'set names' after connect |
347
|
|
|
if ($this->isDbMysql()) { |
348
|
|
|
$lang = $this->profile['lang']; |
349
|
|
|
$names = str_replace('UTF-8', 'UTF8', strtoupper($lang)); |
350
|
|
|
$sql = "SET NAMES '{$names}'"; |
351
|
|
|
$this->conn->Execute($sql); |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
// @codeCoverageIgnoreEnd |
355
|
|
|
|
356
|
|
|
return true; |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* Convert encoding from db to sys |
362
|
|
|
* |
363
|
|
|
* Mostly used on query result. |
364
|
|
|
* |
365
|
|
|
* @param array|string &$result Array or string, not RecordSet object |
366
|
|
|
* @return array|string |
367
|
|
|
*/ |
368
|
|
View Code Duplication |
public function convertEncodingResult(&$result) |
|
|
|
|
369
|
|
|
{ |
370
|
|
|
if (empty($result) || $this->charsetPhp == $this->profile['lang']) { |
371
|
|
|
return $result; |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
if (is_array($result)) { |
375
|
|
|
foreach ($result as &$value) { |
376
|
|
|
$this->convertEncodingResult($value); |
377
|
|
|
} |
378
|
|
|
unset($value); |
379
|
|
|
|
380
|
|
|
} elseif (is_string($result)) { |
381
|
|
|
$result = mb_convert_encoding( |
382
|
|
|
$result, |
383
|
|
|
$this->charsetPhp, |
384
|
|
|
$this->profile['lang'] |
385
|
|
|
); |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
return $result; |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* Convert encoding from sys to db |
394
|
|
|
* |
395
|
|
|
* Mostly used on SQL statement. |
396
|
|
|
* |
397
|
|
|
* @param mixed &$sql |
398
|
|
|
* @return mixed |
399
|
|
|
*/ |
400
|
|
View Code Duplication |
public function convertEncodingSql(&$sql) |
|
|
|
|
401
|
|
|
{ |
402
|
|
|
if (empty($sql) || $this->charsetPhp == $this->profile['lang']) { |
403
|
|
|
return $sql; |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
if (is_array($sql)) { |
407
|
|
|
foreach ($sql as &$val) { |
408
|
|
|
$this->convertEncodingSql($val); |
409
|
|
|
} |
410
|
|
|
unset($val); |
411
|
|
|
} elseif (is_string($sql)) { |
412
|
|
|
$sql = mb_convert_encoding( |
413
|
|
|
$sql, |
414
|
|
|
$this->profile['lang'], |
415
|
|
|
$this->charsetPhp |
416
|
|
|
); |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
return $sql; |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
|
423
|
|
|
/** |
424
|
|
|
* Count how many db query have executed |
425
|
|
|
* |
426
|
|
|
* Can be extend to count on multi db objects. |
427
|
|
|
* |
428
|
|
|
* @param int $step |
429
|
|
|
*/ |
430
|
|
|
protected function countQuery($step = 1) |
431
|
|
|
{ |
432
|
|
|
$this->queryCount += $step; |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
|
436
|
|
|
/** |
437
|
|
|
* Delete rows by condition user given |
438
|
|
|
* |
439
|
|
|
* Return value: |
440
|
|
|
* -1 error, |
441
|
|
|
* 0 not found, |
442
|
|
|
* N > 0 number of deleted rows. |
443
|
|
|
* |
444
|
|
|
* @param string $table |
445
|
|
|
* @param string $condition Not empty, can be raw sql where, having etc |
446
|
|
|
* @return int |
447
|
|
|
*/ |
448
|
|
View Code Duplication |
public function deleteRow($table, $condition) |
|
|
|
|
449
|
|
|
{ |
450
|
|
|
$condition = trim($condition); |
451
|
|
|
if (empty($condition)) { |
452
|
|
|
return -1; |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
$this->executePrepare( |
456
|
|
|
$this->getSqlGenerator()->get(['DELETE' => $table]) |
457
|
|
|
. ' ' . $condition |
458
|
|
|
); |
459
|
|
|
|
460
|
|
|
if (0 != $this->conn->ErrorNo()) { |
461
|
|
|
// @codeCoverageIgnoreStart |
462
|
|
|
// Error is rollbacked, no errorno return |
463
|
|
|
|
464
|
|
|
// Execute error |
465
|
|
|
return -1; |
466
|
|
|
|
467
|
|
|
// @codeCoverageIgnoreEnd |
468
|
|
|
} else { |
469
|
|
|
return $this->conn->Affected_Rows(); |
470
|
|
|
} |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
|
474
|
|
|
/** |
475
|
|
|
* Alias of errorMessage() for backward compatible |
476
|
|
|
*/ |
477
|
|
|
public function errorMsg() |
478
|
|
|
{ |
479
|
|
|
return $this->getErrorMessage(); |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
|
483
|
|
|
/** |
484
|
|
|
* Alias of errorCode() for backward compatible |
485
|
|
|
*/ |
486
|
|
|
public function errorNo() |
487
|
|
|
{ |
488
|
|
|
return $this->getErrorCode(); |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
|
492
|
|
|
/** |
493
|
|
|
* Execute SQL, without transaction |
494
|
|
|
* |
495
|
|
|
* @param mixed $sql SQL statement or sqlCfg for SqlGenerator |
496
|
|
|
* @param mixed $inputArr |
497
|
|
|
* @return object |
498
|
|
|
*/ |
499
|
|
|
public function execute($sql, $inputArr = false) |
500
|
|
|
{ |
501
|
|
|
if (is_array($sql)) { |
502
|
|
|
$sql = $this->getSqlGenerator()->get($sql); |
503
|
|
|
} |
504
|
|
|
|
505
|
|
|
$this->convertEncodingSql($sql); |
506
|
|
|
|
507
|
|
|
$this->countQuery(); |
508
|
|
|
|
509
|
|
|
return $this->conn->Execute($sql, $inputArr); |
510
|
|
|
} |
511
|
|
|
|
512
|
|
|
|
513
|
|
|
/** |
514
|
|
|
* Prepare and execute sql, with transaction |
515
|
|
|
* |
516
|
|
|
* @param string $sql |
517
|
|
|
* @param array|boolean $inputArr Optional parameters in sql |
518
|
|
|
* @return object |
519
|
|
|
*/ |
520
|
|
|
public function executePrepare($sql, $inputArr = false) |
521
|
|
|
{ |
522
|
|
|
if (is_array($sql)) { |
523
|
|
|
$sql = $this->getSqlGenerator()->getPrepared($sql); |
524
|
|
|
} |
525
|
|
|
|
526
|
|
|
$this->convertEncodingSql($sql); |
527
|
|
|
|
528
|
|
|
$stmt = $this->conn->Prepare($sql); |
529
|
|
|
|
530
|
|
|
$this->conn->BeginTrans(); |
531
|
|
|
|
532
|
|
|
$rs = $this->conn->Execute($stmt, $inputArr); |
533
|
|
|
|
534
|
|
|
$this->countQuery(); |
535
|
|
|
|
536
|
|
View Code Duplication |
if (0 != $this->conn->ErrorNo()) { |
|
|
|
|
537
|
|
|
// @codeCoverageIgnoreStart |
538
|
|
|
|
539
|
|
|
// Log error |
540
|
|
|
trigger_error( |
541
|
|
|
'ErrorNo: ' . $this->conn->ErrorNo() |
542
|
|
|
. "\nErrorMsg: " . $this->conn->ErrorMsg(), |
543
|
|
|
E_USER_ERROR |
544
|
|
|
); |
545
|
|
|
$this->conn->RollbackTrans(); |
546
|
|
|
|
547
|
|
|
return -1; |
|
|
|
|
548
|
|
|
|
549
|
|
|
// @codeCoverageIgnoreEnd |
550
|
|
|
} else { |
551
|
|
|
$this->conn->CommitTrans(); |
552
|
|
|
|
553
|
|
|
return $rs; |
554
|
|
|
} |
555
|
|
|
} |
556
|
|
|
|
557
|
|
|
|
558
|
|
|
/** |
559
|
|
|
* Generate SQL statement |
560
|
|
|
* |
561
|
|
|
* User should avoid use SELECT/UPDATE/INSERT/DELETE simultaneously. |
562
|
|
|
* |
563
|
|
|
* Generate order by SQL statement format order. |
564
|
|
|
* |
565
|
|
|
* UPDATE/INSERT/DELETE is followed by [TBL_NAME], so need not use FROM. |
566
|
|
|
* |
567
|
|
|
* @see Fwlib\Db\SqlGenerator |
568
|
|
|
* @param array $sqlConfig |
569
|
|
|
* @return string |
570
|
|
|
*/ |
571
|
|
|
public function generateSql($sqlConfig) |
572
|
|
|
{ |
573
|
|
|
if (!empty($sqlConfig)) { |
574
|
|
|
return $this->getSqlGenerator()->get($sqlConfig); |
575
|
|
|
} else { |
576
|
|
|
return ''; |
577
|
|
|
} |
578
|
|
|
} |
579
|
|
|
|
580
|
|
|
|
581
|
|
|
/** |
582
|
|
|
* Generate SQL statement for Prepare |
583
|
|
|
* |
584
|
|
|
* Format like value -> ? or :name, and quote chars removed. |
585
|
|
|
* |
586
|
|
|
* @see generateSql() |
587
|
|
|
* @see Fwlib\Db\SqlGenerator |
588
|
|
|
* @param array $sqlConfig |
589
|
|
|
* @return string |
590
|
|
|
*/ |
591
|
|
|
public function generateSqlPrepared($sqlConfig) |
592
|
|
|
{ |
593
|
|
|
if (!empty($sqlConfig)) { |
594
|
|
|
return $this->getSqlGenerator()->getPrepared($sqlConfig); |
595
|
|
|
} else { |
596
|
|
|
return ''; |
597
|
|
|
} |
598
|
|
|
} |
599
|
|
|
|
600
|
|
|
|
601
|
|
|
/** |
602
|
|
|
* Get single row data from single table using key |
603
|
|
|
* |
604
|
|
|
* Also, this method can be used to retrieve data from a table by primary |
605
|
|
|
* or unique key, default and recommend for primary key, which can be auto |
606
|
|
|
* retrieved from table meta. |
607
|
|
|
* |
608
|
|
|
* Whatever key is used, the result should only contain maximum one row, |
609
|
|
|
* or the result is wrong, commonly only include data of first match row. |
610
|
|
|
* |
611
|
|
|
* |
612
|
|
|
* $keyValue, $column, $keyColumn support multiple value split by ',' or |
613
|
|
|
* array, eg: 'value' or 'value1, value2' or array('value1', 'value2') |
614
|
|
|
* |
615
|
|
|
* $column can use style like 'colName AS colAlias'. |
616
|
|
|
* |
617
|
|
|
* '*' can be used for $column, means all columns in table. |
618
|
|
|
* |
619
|
|
|
* Notice: if $column is array, must indexed by number start from 0. |
620
|
|
|
* |
621
|
|
|
* @param string $table |
622
|
|
|
* @param int|string $keyValue |
623
|
|
|
* @param string|array $column Empty or '*' for all column |
624
|
|
|
* @param string|array $keyColumn Empty to use primary key |
625
|
|
|
* @return int|string|array Single value or array of it, null if error |
626
|
|
|
* occur |
627
|
|
|
*/ |
628
|
|
|
public function getByKey( |
629
|
|
|
$table, |
630
|
|
|
$keyValue, |
631
|
|
|
$column = null, |
632
|
|
|
$keyColumn = [] |
633
|
|
|
) { |
634
|
|
|
$stringUtil = $this->getUtilContainer()->getString(); |
635
|
|
|
|
636
|
|
|
// Treat key column |
637
|
|
|
if (empty($keyColumn)) { |
638
|
|
|
$keyColumn = $this->getMetaPrimaryKey($table); |
639
|
|
|
} |
640
|
|
|
|
641
|
|
|
// Convert key value and column name to array |
642
|
|
|
if (is_string($keyValue)) { |
643
|
|
|
$keyValue = $stringUtil->toArray($keyValue, ','); |
644
|
|
|
} else { |
645
|
|
|
$keyValue = (array)$keyValue; |
646
|
|
|
} |
647
|
|
|
|
648
|
|
|
if (is_string($keyColumn)) { |
649
|
|
|
$keyColumn = $stringUtil->toArray($keyColumn, ','); |
650
|
|
|
} else { |
651
|
|
|
$keyColumn = (array)$keyColumn; |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
// $keyColumn need to be array same count with $keyValue |
655
|
|
|
if (count($keyValue) != count($keyColumn)) { |
656
|
|
|
// @codeCoverageIgnoreStart |
657
|
|
|
trigger_error('Key value and column not match.', E_USER_WARNING); |
658
|
|
|
|
659
|
|
|
return null; |
660
|
|
|
// @codeCoverageIgnoreEnd |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
|
664
|
|
|
if (empty($column) || '*' == $column) { |
665
|
|
|
// Drop uppercase index |
666
|
|
|
$column = array_values($this->getMetaColumnName($table)); |
667
|
|
|
|
668
|
|
|
} elseif (!is_array($column)) { |
669
|
|
|
if (is_string($column)) { |
670
|
|
|
$column = $stringUtil->toArray($column, ','); |
671
|
|
|
} else { |
672
|
|
|
// Column is not array nor string? is int? should not happen |
673
|
|
|
// @codeCoverageIgnoreStart |
674
|
|
|
$column = [$column]; |
675
|
|
|
// @codeCoverageIgnoreEnd |
676
|
|
|
} |
677
|
|
|
} |
678
|
|
|
|
679
|
|
|
// $keyValue, $column, $keyColumn all converted to array |
680
|
|
|
|
681
|
|
|
|
682
|
|
|
// Retrieve from db |
683
|
|
|
$sqlConfig = [ |
684
|
|
|
'SELECT' => $column, |
685
|
|
|
'FROM' => $table, |
686
|
|
|
'LIMIT' => 1, |
687
|
|
|
]; |
688
|
|
|
while (!empty($keyValue)) { |
689
|
|
|
$singleKey = array_shift($keyColumn); |
690
|
|
|
$sqlConfig['WHERE'][] = $singleKey . ' = ' |
691
|
|
|
. $this->quoteValue($table, $singleKey, array_shift($keyValue)); |
692
|
|
|
unset($singleKey); |
693
|
|
|
} |
694
|
|
|
$rs = $this->execute($sqlConfig); |
695
|
|
|
$ar = []; |
696
|
|
|
if (!empty($rs) && !$rs->EOF) { |
697
|
|
|
$ar = $rs->FetchRow(); |
698
|
|
|
} |
699
|
|
|
|
700
|
|
|
// Return value |
701
|
|
|
if (empty($ar)) { |
702
|
|
|
return null; |
703
|
|
|
|
704
|
|
|
} else { |
705
|
|
|
if (1 == count($ar)) { |
706
|
|
|
return array_pop($ar); |
707
|
|
|
|
708
|
|
|
} else { |
709
|
|
|
return $ar; |
710
|
|
|
} |
711
|
|
|
} |
712
|
|
|
} |
713
|
|
|
|
714
|
|
|
|
715
|
|
|
/** |
716
|
|
|
* Dummy for ADOdb's ErrorNo() |
717
|
|
|
* |
718
|
|
|
* @return int |
719
|
|
|
*/ |
720
|
|
|
public function getErrorCode() |
721
|
|
|
{ |
722
|
|
|
return $this->conn->ErrorNo(); |
723
|
|
|
} |
724
|
|
|
|
725
|
|
|
|
726
|
|
|
/** |
727
|
|
|
* Dummy for ADOdb's ErrorMsg() |
728
|
|
|
* |
729
|
|
|
* @return string |
730
|
|
|
*/ |
731
|
|
|
public function getErrorMessage() |
732
|
|
|
{ |
733
|
|
|
return $this->conn->ErrorMsg(); |
734
|
|
|
} |
735
|
|
|
|
736
|
|
|
|
737
|
|
|
/** |
738
|
|
|
* Get table schema |
739
|
|
|
* |
740
|
|
|
* @see $metaColumn |
741
|
|
|
* @param string $table |
742
|
|
|
* @param boolean $forcenew Force to retrieve instead of read from cache |
743
|
|
|
* @return array |
744
|
|
|
*/ |
745
|
|
|
public function getMetaColumn($table, $forcenew = false) |
746
|
|
|
{ |
747
|
|
|
if (!isset($this->metaColumn[$table]) || (true == $forcenew)) { |
|
|
|
|
748
|
|
|
$this->metaColumn[$table] = $this->conn->MetaColumns($table); |
749
|
|
|
if (empty($this->metaColumn[$table])) { |
750
|
|
|
return null; |
751
|
|
|
} |
752
|
|
|
|
753
|
|
|
// Convert columns to native case |
754
|
|
|
$colName = $this->getMetaColumnName($table, $forcenew); |
755
|
|
|
// $colName = array(COLUMN => column), $c is UPPER CASED |
756
|
|
|
$art = []; |
757
|
|
|
foreach ($this->metaColumn[$table] as $c => $ar) { |
758
|
|
|
$art[$colName[strtoupper($c)]] = $ar; |
759
|
|
|
} |
760
|
|
|
$this->metaColumn[$table] = $art; |
761
|
|
|
|
762
|
|
|
// @codeCoverageIgnoreStart |
763
|
|
|
// Fix sybase display timestamp column as varbinary |
764
|
|
View Code Duplication |
if ($this->isDbSybase()) { |
|
|
|
|
765
|
|
|
$s = $this->getMetaTimestamp($table); |
766
|
|
|
if (!empty($s)) { |
767
|
|
|
$this->metaColumn[$table][$s]->type = 'timestamp'; |
768
|
|
|
} |
769
|
|
|
} |
770
|
|
|
// @codeCoverageIgnoreEnd |
771
|
|
|
} |
772
|
|
|
|
773
|
|
|
return $this->metaColumn[$table]; |
774
|
|
|
} |
775
|
|
|
|
776
|
|
|
|
777
|
|
|
/** |
778
|
|
|
* Get table column name |
779
|
|
|
* |
780
|
|
|
* @see $metaColumnName |
781
|
|
|
* @param string $table |
782
|
|
|
* @param boolean $forcenew Force to retrieve instead of read from cache |
783
|
|
|
* @return array |
784
|
|
|
*/ |
785
|
|
|
public function getMetaColumnName($table, $forcenew = false) |
786
|
|
|
{ |
787
|
|
View Code Duplication |
if (!isset($this->metaColumnName[$table]) || (true == $forcenew)) { |
|
|
|
|
788
|
|
|
$this->metaColumnName[$table] = $this->conn->MetaColumnNames($table); |
789
|
|
|
} |
790
|
|
|
|
791
|
|
|
return $this->metaColumnName[$table]; |
792
|
|
|
} |
793
|
|
|
|
794
|
|
|
|
795
|
|
|
/** |
796
|
|
|
* Get primary key column of a table |
797
|
|
|
* |
798
|
|
|
* Return single string value or array for multi column primary key. |
799
|
|
|
* |
800
|
|
|
* @param string $table |
801
|
|
|
* @param boolean $forcenew Force to retrieve instead of read from cache |
802
|
|
|
* @return mixed |
803
|
|
|
* @see $metaPrimaryKey |
804
|
|
|
*/ |
805
|
|
|
public function getMetaPrimaryKey($table, $forcenew = false) |
806
|
|
|
{ |
807
|
|
|
if (!isset($this->metaPrimaryKey[$table]) || (true == $forcenew)) { |
|
|
|
|
808
|
|
|
// @codeCoverageIgnoreStart |
809
|
|
View Code Duplication |
if ($this->isDbSybase()) { |
|
|
|
|
810
|
|
|
/** |
811
|
|
|
* MetaPrimaryKey() in ADOdb has error(till v5.18), find PK |
812
|
|
|
* manually. |
813
|
|
|
* |
814
|
|
|
* @link http://topic.csdn.net/t/20030117/17/1369396.html |
815
|
|
|
* |
816
|
|
|
* SELECT name, keycnt, |
817
|
|
|
* index_col(tableName, indid, 1), -- 1st PK col |
818
|
|
|
* index_col(tableName, indid, 2) -- 2nd PK col if has |
819
|
|
|
* FROM sysindexes |
820
|
|
|
* WHERE status & 2048 = 2048 |
821
|
|
|
* AND id = object_id(tableName) |
822
|
|
|
* |
823
|
|
|
* keycnt is column count in PK. if PK index is not cursor |
824
|
|
|
* index(by 0x10 bit in status), its keycnt - 1. |
825
|
|
|
* |
826
|
|
|
* Test pass for PK include 2 columns. |
827
|
|
|
*/ |
828
|
|
|
$rs = $this->execute( |
829
|
|
|
[ |
830
|
|
|
'SELECT' => [ |
831
|
|
|
'name' => 'a.name', |
832
|
|
|
'keycnt' => 'a.keycnt', |
833
|
|
|
'k1' => "index_col('$table', indid, 1)", |
834
|
|
|
'k2' => "index_col('$table', indid, 2)", |
835
|
|
|
'k3' => "index_col('$table', indid, 3)", |
836
|
|
|
], |
837
|
|
|
'FROM' => [ |
838
|
|
|
'a' => 'sysindexes', |
839
|
|
|
'b' => 'sysobjects', |
840
|
|
|
], |
841
|
|
|
'WHERE' => [ |
842
|
|
|
'a.status & 2048 = 2048 ', |
843
|
|
|
"b.name = '$table'", |
844
|
|
|
"a.id = b.id", |
845
|
|
|
], |
846
|
|
|
] |
847
|
|
|
); |
848
|
|
|
if (true == $rs && 0 < $rs->RowCount()) { |
849
|
|
|
// Got |
850
|
|
|
$ar = [$rs->fields['k1']]; |
851
|
|
|
if (!empty($rs->fields['k2'])) { |
852
|
|
|
$ar[] = $rs->fields['k2']; |
853
|
|
|
} |
854
|
|
|
if (!empty($rs->fields['k3'])) { |
855
|
|
|
$ar[] = $rs->fields['k3']; |
856
|
|
|
} |
857
|
|
|
} else { |
858
|
|
|
// Table have no primary key |
859
|
|
|
$ar = ''; |
860
|
|
|
} |
861
|
|
|
|
862
|
|
|
} else { |
863
|
|
|
// Find using ADOdb first |
864
|
|
|
$ar = $this->conn->MetaPrimaryKeys($table); |
865
|
|
|
} |
866
|
|
|
// @codeCoverageIgnoreEnd |
867
|
|
|
|
868
|
|
|
|
869
|
|
|
// Convert columns to native case |
870
|
|
|
if (!empty($ar)) { |
871
|
|
|
$colName = $this->GetMetaColumnName($table); |
872
|
|
|
// $colName = array(COLUMN => column), $c is UPPER CASED |
873
|
|
|
$art = []; |
874
|
|
|
foreach ($ar as $idx => $col) { |
875
|
|
|
$art[] = $colName[strtoupper($col)]; |
876
|
|
|
} |
877
|
|
|
$ar = $art; |
878
|
|
|
} |
879
|
|
|
|
880
|
|
|
if (is_array($ar) && 1 == count($ar)) { |
881
|
|
|
// Only 1 primary key column |
882
|
|
|
$ar = $ar[0]; |
883
|
|
|
} |
884
|
|
|
|
885
|
|
|
// Set to cache |
886
|
|
|
if (!empty($ar)) { |
887
|
|
|
$this->metaPrimaryKey[$table] = $ar; |
888
|
|
|
} |
889
|
|
|
} |
890
|
|
|
|
891
|
|
|
if (isset($this->metaPrimaryKey[$table])) { |
892
|
|
|
return $this->metaPrimaryKey[$table]; |
893
|
|
|
|
894
|
|
|
} else { |
895
|
|
|
return null; |
896
|
|
|
} |
897
|
|
|
} |
898
|
|
|
|
899
|
|
|
|
900
|
|
|
/** |
901
|
|
|
* Get name of timestamp column of a table |
902
|
|
|
* |
903
|
|
|
* Timestamp column are various for different db, hard to test. |
904
|
|
|
* |
905
|
|
|
* @param $table |
906
|
|
|
* @return string |
907
|
|
|
*/ |
908
|
|
|
public function getMetaTimestamp($table) |
909
|
|
|
{ |
910
|
|
|
$arCol = $this->getMetaColumn($table); |
911
|
|
|
if (empty($arCol)) { |
912
|
|
|
return ''; |
913
|
|
|
} |
914
|
|
|
|
915
|
|
|
// @codeCoverageIgnoreStart |
916
|
|
|
if ($this->isDbSybase()) { |
917
|
|
|
// Sybase timestamp column must be lower cased. |
918
|
|
|
// If col name is 'timestamp', will auto assign (timestamp) type. |
919
|
|
|
$rs = $this->execute( |
920
|
|
|
[ |
921
|
|
|
'SELECT' => [ |
922
|
|
|
'name' => 'a.name', |
923
|
|
|
'length' => 'a.length', |
924
|
|
|
'usertype' => 'a.usertype', |
925
|
|
|
'type' => 'b.name', |
926
|
|
|
'tableName' => 'c.name', |
927
|
|
|
], |
928
|
|
|
'FROM' => [ |
929
|
|
|
'a' => 'syscolumns', |
930
|
|
|
'b' => 'systypes', |
931
|
|
|
'c' => 'sysobjects', |
932
|
|
|
], |
933
|
|
|
'WHERE' => [ |
934
|
|
|
"a.id = c.id", |
935
|
|
|
'a.type = b.type', |
936
|
|
|
'a.usertype = b.usertype', |
937
|
|
|
'b.type = 37', |
938
|
|
|
'b.usertype = 80', |
939
|
|
|
], |
940
|
|
|
] |
941
|
|
|
); |
942
|
|
View Code Duplication |
while (!empty($rs) && !$rs->EOF) { |
|
|
|
|
943
|
|
|
if ($table == $rs->fields['tableName']) { |
944
|
|
|
return $rs->fields['name']; |
945
|
|
|
} |
946
|
|
|
$rs->MoveNext(); |
947
|
|
|
} |
948
|
|
|
|
949
|
|
|
return ''; |
950
|
|
|
|
951
|
|
View Code Duplication |
} elseif ($this->isDbMysql()) { |
|
|
|
|
952
|
|
|
// Check 'type' |
953
|
|
|
foreach ($arCol as $k => $v) { |
954
|
|
|
if (isset($v->type) && 'timestamp' == $v->type) { |
955
|
|
|
return $k; |
956
|
|
|
} |
957
|
|
|
} |
958
|
|
|
|
959
|
|
|
} else { |
960
|
|
|
// Do not trigger error, null means no implemented. |
961
|
|
|
// Use '||' to fool code inspection. |
962
|
|
|
return null || |
|
|
|
|
963
|
|
|
trigger_error( |
964
|
|
|
__CLASS__ . '::getMetaTimestamp() for ' |
965
|
|
|
. $this->profile['type'] |
966
|
|
|
. ' not implemented!', |
967
|
|
|
E_USER_ERROR |
968
|
|
|
); |
969
|
|
|
} |
970
|
|
|
// @codeCoverageIgnoreEnd |
971
|
|
|
|
972
|
|
|
// No timestamp found |
973
|
|
|
return ''; |
974
|
|
|
} |
975
|
|
|
|
976
|
|
|
|
977
|
|
|
/** |
978
|
|
|
* Getter of $profile |
979
|
|
|
* |
980
|
|
|
* @return array |
981
|
|
|
*/ |
982
|
|
|
public function getProfile() |
983
|
|
|
{ |
984
|
|
|
return $this->profile; |
985
|
|
|
} |
986
|
|
|
|
987
|
|
|
|
988
|
|
|
/** |
989
|
|
|
* Get string describe of profile |
990
|
|
|
* |
991
|
|
|
* Usually used for identify db source. |
992
|
|
|
* |
993
|
|
|
* @param string $separator |
994
|
|
|
* @return string |
995
|
|
|
*/ |
996
|
|
|
public function getProfileString($separator = '-') |
997
|
|
|
{ |
998
|
|
|
return $this->profile['type'] . $separator . |
999
|
|
|
$this->profile['host'] . $separator . |
1000
|
|
|
$this->profile['name']; |
1001
|
|
|
} |
1002
|
|
|
|
1003
|
|
|
|
1004
|
|
|
/** |
1005
|
|
|
* Getter of $queryCount |
1006
|
|
|
* |
1007
|
|
|
* @return int |
1008
|
|
|
*/ |
1009
|
|
|
public function getQueryCount() |
1010
|
|
|
{ |
1011
|
|
|
return $this->queryCount; |
1012
|
|
|
} |
1013
|
|
|
|
1014
|
|
|
|
1015
|
|
|
/** |
1016
|
|
|
* Get rows count by condition user given |
1017
|
|
|
* |
1018
|
|
|
* Return value: |
1019
|
|
|
* -1: error, |
1020
|
|
|
* N >= 0: number of rows. |
1021
|
|
|
* |
1022
|
|
|
* @param string $table |
1023
|
|
|
* @param string $condition Raw sql, can be WHERE, HAVING etc |
1024
|
|
|
* @return int |
1025
|
|
|
*/ |
1026
|
|
View Code Duplication |
public function getRowCount($table, $condition = '') |
|
|
|
|
1027
|
|
|
{ |
1028
|
|
|
$sqlCfg = [ |
1029
|
|
|
'SELECT' => ['c' => 'COUNT(1)'], |
1030
|
|
|
'FROM' => $table, |
1031
|
|
|
]; |
1032
|
|
|
$rs = $this->executePrepare( |
1033
|
|
|
$this->getSqlGenerator()->get($sqlCfg) |
1034
|
|
|
. ' ' . $condition |
1035
|
|
|
); |
1036
|
|
|
if (false == $rs || 0 != $this->conn->ErrorNo() |
1037
|
|
|
|| 0 == $rs->RowCount() |
1038
|
|
|
) { |
1039
|
|
|
// Execute error, rare happen |
1040
|
|
|
// @codeCoverageIgnoreStart |
1041
|
|
|
return -1; |
1042
|
|
|
// @codeCoverageIgnoreEnd |
1043
|
|
|
} else { |
1044
|
|
|
return $rs->fields['c']; |
1045
|
|
|
} |
1046
|
|
|
} |
1047
|
|
|
|
1048
|
|
|
|
1049
|
|
|
/** |
1050
|
|
|
* Get delimiter between SQL for various db |
1051
|
|
|
* |
1052
|
|
|
* @param string $tail Tail of line for eye candy |
1053
|
|
|
* @return string |
1054
|
|
|
*/ |
1055
|
|
|
public function getSqlDelimiter($tail = "\n") |
1056
|
|
|
{ |
1057
|
|
|
// @codeCoverageIgnoreStart |
1058
|
|
|
if ($this->isDbMysql()) { |
1059
|
|
|
$delimiter = ';'; |
1060
|
|
|
|
1061
|
|
|
} elseif ($this->isDbSybase()) { |
1062
|
|
|
$delimiter = ''; |
1063
|
|
|
|
1064
|
|
|
} else { |
1065
|
|
|
trigger_error( |
1066
|
|
|
__CLASS__ . '::getSqlDelimiter() for db type ' |
1067
|
|
|
. $this->profile['type'] . ' not implement.', |
1068
|
|
|
E_USER_WARNING |
1069
|
|
|
); |
1070
|
|
|
$delimiter = ''; |
1071
|
|
|
} |
1072
|
|
|
|
1073
|
|
|
// @codeCoverageIgnoreEnd |
1074
|
|
|
|
1075
|
|
|
return $delimiter . $tail; |
1076
|
|
|
} |
1077
|
|
|
|
1078
|
|
|
|
1079
|
|
|
/** |
1080
|
|
|
* Get SqlGenerator instance |
1081
|
|
|
* |
1082
|
|
|
* @return SqlGenerator |
1083
|
|
|
*/ |
1084
|
|
|
protected function getSqlGenerator() |
1085
|
|
|
{ |
1086
|
|
|
if (is_null($this->sqlGenerator)) { |
1087
|
|
|
$this->sqlGenerator = new SqlGenerator($this); |
1088
|
|
|
} |
1089
|
|
|
|
1090
|
|
|
return $this->sqlGenerator; |
1091
|
|
|
} |
1092
|
|
|
|
1093
|
|
|
|
1094
|
|
|
/** |
1095
|
|
|
* Get SQL: begin transaction |
1096
|
|
|
* |
1097
|
|
|
* @return string |
1098
|
|
|
*/ |
1099
|
|
|
public function getSqlTransBegin() |
1100
|
|
|
{ |
1101
|
|
|
// @codeCoverageIgnoreStart |
1102
|
|
|
if ($this->isDbMysql()) { |
1103
|
|
|
$header = 'START'; |
1104
|
|
|
} else { |
1105
|
|
|
$header = 'BEGIN'; |
1106
|
|
|
} |
1107
|
|
|
|
1108
|
|
|
// @codeCoverageIgnoreEnd |
1109
|
|
|
|
1110
|
|
|
return $header . ' TRANSACTION' . $this->getSqlDelimiter(); |
1111
|
|
|
} |
1112
|
|
|
|
1113
|
|
|
|
1114
|
|
|
/** |
1115
|
|
|
* Get SQL: commit transaction |
1116
|
|
|
* |
1117
|
|
|
* @return string |
1118
|
|
|
*/ |
1119
|
|
|
public function getSqlTransCommit() |
1120
|
|
|
{ |
1121
|
|
|
return 'COMMIT' . $this->getSqlDelimiter(); |
1122
|
|
|
} |
1123
|
|
|
|
1124
|
|
|
|
1125
|
|
|
/** |
1126
|
|
|
* Get SQL: rollback transaction |
1127
|
|
|
* |
1128
|
|
|
* @return string |
1129
|
|
|
*/ |
1130
|
|
|
public function getSqlTransRollback() |
1131
|
|
|
{ |
1132
|
|
|
return 'ROLLBACK' . $this->GetSqlDelimiter(); |
1133
|
|
|
} |
1134
|
|
|
|
1135
|
|
|
|
1136
|
|
|
/** |
1137
|
|
|
* If current db is connected successful |
1138
|
|
|
* |
1139
|
|
|
* @return boolean |
1140
|
|
|
*/ |
1141
|
|
|
public function isConnected() |
1142
|
|
|
{ |
1143
|
|
|
return $this->conn->IsConnected(); |
1144
|
|
|
} |
1145
|
|
|
|
1146
|
|
|
|
1147
|
|
|
/** |
1148
|
|
|
* If current db type is mysql |
1149
|
|
|
* |
1150
|
|
|
* @return boolean |
1151
|
|
|
*/ |
1152
|
|
|
public function isDbMysql() |
1153
|
|
|
{ |
1154
|
|
|
return ('mysql' == substr($this->conn->databaseType, 0, 5)); |
1155
|
|
|
} |
1156
|
|
|
|
1157
|
|
|
|
1158
|
|
|
/** |
1159
|
|
|
* If current db type is sybase and connect with PDO (DBLIB) |
1160
|
|
|
* |
1161
|
|
|
* @return bool |
1162
|
|
|
*/ |
1163
|
|
|
public function isDbPdoSybase() |
1164
|
|
|
{ |
1165
|
|
|
return ('pdo_sybase' == substr($this->profile['type'], 0, 10)) || |
1166
|
|
|
('pdo_sybase_ase' == substr($this->profile['type'], 0, 14)); |
1167
|
|
|
} |
1168
|
|
|
|
1169
|
|
|
|
1170
|
|
|
/** |
1171
|
|
|
* If current db type is sybase |
1172
|
|
|
* |
1173
|
|
|
* @return boolean |
1174
|
|
|
*/ |
1175
|
|
|
public function isDbSybase() |
1176
|
|
|
{ |
1177
|
|
|
return ('sybase' == substr($this->profile['type'], 0, 6)) || |
1178
|
|
|
('sybase_ase' == substr($this->profile['type'], 0, 10)) || |
1179
|
|
|
('pdo_sybase' == substr($this->profile['type'], 0, 10)) || |
1180
|
|
|
('pdo_sybase_ase' == substr($this->profile['type'], 0, 14)); |
1181
|
|
|
} |
1182
|
|
|
|
1183
|
|
|
|
1184
|
|
|
/** |
1185
|
|
|
* If a table exists in db ? |
1186
|
|
|
* |
1187
|
|
|
* @param string $table |
1188
|
|
|
* @return boolean |
1189
|
|
|
*/ |
1190
|
|
View Code Duplication |
public function isTableExist($table) |
|
|
|
|
1191
|
|
|
{ |
1192
|
|
|
$table = addslashes($table); |
1193
|
|
|
|
1194
|
|
|
// @codeCoverageIgnoreStart |
1195
|
|
|
if ($this->isDbSybase()) { |
1196
|
|
|
$sql = "SELECT count(1) AS c FROM sysobjects WHERE name = |
1197
|
|
|
'{$table}' AND type = 'U'"; |
1198
|
|
|
$rs = $this->execute($sql); |
1199
|
|
|
|
1200
|
|
|
return (0 != $rs->fields['c']); |
1201
|
|
|
|
1202
|
|
|
} elseif ($this->isDbMysql()) { |
1203
|
|
|
$sql = "SHOW TABLES LIKE '$table'"; |
1204
|
|
|
$rs = $this->execute($sql); |
1205
|
|
|
|
1206
|
|
|
return (0 != $rs->RowCount()); |
1207
|
|
|
|
1208
|
|
|
} else { |
1209
|
|
|
// :THINK: Better method ? |
1210
|
|
|
$sql = "SELECT 1 FROM $table"; |
1211
|
|
|
$this->execute($sql); |
1212
|
|
|
|
1213
|
|
|
return (0 == $this->conn->ErrorNo()); |
1214
|
|
|
} |
1215
|
|
|
// @codeCoverageIgnoreEnd |
1216
|
|
|
} |
1217
|
|
|
|
1218
|
|
|
|
1219
|
|
|
/** |
1220
|
|
|
* If timestamp column's value is unique |
1221
|
|
|
* |
1222
|
|
|
* @return boolean |
1223
|
|
|
*/ |
1224
|
|
|
public function isTimestampUnique() |
1225
|
|
|
{ |
1226
|
|
|
// Default for: sybase |
1227
|
|
|
$b = true; |
1228
|
|
|
|
1229
|
|
|
// @codeCoverageIgnoreStart |
1230
|
|
|
if ($this->isDbMysql()) { |
1231
|
|
|
$b = false; |
1232
|
|
|
} |
1233
|
|
|
|
1234
|
|
|
// @codeCoverageIgnoreEnd |
1235
|
|
|
|
1236
|
|
|
return $b; |
1237
|
|
|
} |
1238
|
|
|
|
1239
|
|
|
|
1240
|
|
|
/** |
1241
|
|
|
* Generate a bind placeholder portable |
1242
|
|
|
* |
1243
|
|
|
* @param string $name |
1244
|
|
|
* @return string |
1245
|
|
|
*/ |
1246
|
|
|
public function param($name) |
1247
|
|
|
{ |
1248
|
|
|
$param = $this->conn->Param($name); |
1249
|
|
|
|
1250
|
|
|
if ($this->isDbPdoSybase()) { |
1251
|
|
|
$param = ":{$name}"; |
1252
|
|
|
} |
1253
|
|
|
|
1254
|
|
|
return $param; |
1255
|
|
|
} |
1256
|
|
|
|
1257
|
|
|
|
1258
|
|
|
/** |
1259
|
|
|
* Generate a bind placeholder portable, for PDO only |
1260
|
|
|
* |
1261
|
|
|
* Will add CONVERT() for int/numeric data type |
1262
|
|
|
* |
1263
|
|
|
* @param string $table |
1264
|
|
|
* @param string $name |
1265
|
|
|
* @return string |
1266
|
|
|
*/ |
1267
|
|
|
public function pdoParam($table, $name) |
1268
|
|
|
{ |
1269
|
|
|
$param = $this->conn->Param($name); |
1270
|
|
|
|
1271
|
|
|
if ($this->isDbPdoSybase()) { |
1272
|
|
|
$param = ":{$name}"; |
1273
|
|
|
|
1274
|
|
|
// Add explicit convert by data type |
1275
|
|
|
$this->getMetaColumn($table); |
1276
|
|
|
if (!isset($this->metaColumn[$table][$name]->type)) { |
1277
|
|
|
throw new \Exception( |
1278
|
|
|
"Column to quote not exists($table.$name)." |
1279
|
|
|
); |
1280
|
|
|
} |
1281
|
|
|
|
1282
|
|
|
$columnMeta = $this->metaColumn[$table][$name]; |
1283
|
|
|
$type = $columnMeta->type; |
1284
|
|
|
$precision = $columnMeta->precision; |
1285
|
|
|
$scale = $columnMeta->scale; |
1286
|
|
|
if (in_array($type, [ |
1287
|
|
|
'bigint', |
1288
|
|
|
'bit', |
1289
|
|
|
'int', |
1290
|
|
|
'intn', // Sybase - tinyint |
1291
|
|
|
'mediumint', |
1292
|
|
|
'smallint', |
1293
|
|
|
'tinyint', |
1294
|
|
|
])) { |
1295
|
|
|
$type = rtrim($type, 'n'); |
1296
|
|
|
$param = "CONVERT({$type}, $param)"; |
1297
|
|
|
|
1298
|
|
|
} elseif (in_array($type, [ |
1299
|
|
|
'decimal', |
1300
|
|
|
'double', |
1301
|
|
|
'float', |
1302
|
|
|
'numeric', |
1303
|
|
|
'numericn', // Sybase - numeric |
1304
|
|
|
'real', |
1305
|
|
|
])) { |
1306
|
|
|
$type = rtrim($type, 'n'); |
1307
|
|
|
$param = "CONVERT({$type}({$precision}, {$scale}), $param)"; |
1308
|
|
|
} |
1309
|
|
|
} |
1310
|
|
|
|
1311
|
|
|
return $param; |
1312
|
|
|
} |
1313
|
|
|
|
1314
|
|
|
|
1315
|
|
|
/** |
1316
|
|
|
* Smart quote string in sql, by check columns type |
1317
|
|
|
* |
1318
|
|
|
* @param string $table |
1319
|
|
|
* @param string $col |
1320
|
|
|
* @param mixed $val |
1321
|
|
|
* @return string |
1322
|
|
|
*/ |
1323
|
|
View Code Duplication |
public function quoteValue($table, $col, $val) |
|
|
|
|
1324
|
|
|
{ |
1325
|
|
|
$this->getMetaColumn($table); |
1326
|
|
|
if (!isset($this->metaColumn[$table][$col]->type)) { |
1327
|
|
|
trigger_error( |
1328
|
|
|
"Column to quote not exists($table.$col).", |
1329
|
|
|
E_USER_WARNING |
1330
|
|
|
); |
1331
|
|
|
|
1332
|
|
|
// @codeCoverageIgnoreStart |
1333
|
|
|
// Return quoted value for safety |
1334
|
|
|
$val = stripslashes($val); |
1335
|
|
|
|
1336
|
|
|
return $this->conn->qstr($val, false); |
1337
|
|
|
// @codeCoverageIgnoreEnd |
1338
|
|
|
} |
1339
|
|
|
|
1340
|
|
|
$type = $this->metaColumn[$table][$col]->type; |
1341
|
|
|
if (in_array( |
1342
|
|
|
$type, |
1343
|
|
|
[ |
1344
|
|
|
'bigint', |
1345
|
|
|
'bit', |
1346
|
|
|
'decimal', |
1347
|
|
|
'double', |
1348
|
|
|
'float', |
1349
|
|
|
'int', |
1350
|
|
|
'intn', // Sybase - tinyint |
1351
|
|
|
'mediumint', |
1352
|
|
|
'numeric', |
1353
|
|
|
'numericn', // Sybase - numeric |
1354
|
|
|
'real', |
1355
|
|
|
'smallint', |
1356
|
|
|
'tinyint', |
1357
|
|
|
] |
1358
|
|
|
)) { |
1359
|
|
|
// Need not quote, output directly |
1360
|
|
|
return $val; |
1361
|
|
|
|
1362
|
|
|
} elseif ($this->isDbSybase() && 'timestamp' == $type) { |
1363
|
|
|
// Sybase timestamp |
1364
|
|
|
// @codeCoverageIgnoreStart |
1365
|
|
|
return '0x' . $val; |
1366
|
|
|
//elseif ($this->IsDbSybase() && 'varbinary' == $type && 'timestamp' == $col) |
1367
|
|
|
// @codeCoverageIgnoreEnd |
1368
|
|
|
|
1369
|
|
|
} else { |
1370
|
|
|
// Need quote, use db's quote method |
1371
|
|
|
$val = stripslashes($val); |
1372
|
|
|
|
1373
|
|
|
return $this->conn->qstr($val, false); |
1374
|
|
|
} |
1375
|
|
|
} |
1376
|
|
|
|
1377
|
|
|
|
1378
|
|
|
/** |
1379
|
|
|
* Set PHP script file charset |
1380
|
|
|
* |
1381
|
|
|
* @param string $charset |
1382
|
|
|
* @see $charsetPhp |
1383
|
|
|
*/ |
1384
|
|
|
public function setCharsetPhp($charset) |
1385
|
|
|
{ |
1386
|
|
|
$this->charsetPhp = $charset; |
1387
|
|
|
} |
1388
|
|
|
|
1389
|
|
|
|
1390
|
|
|
/** |
1391
|
|
|
* Setter of fetchMode |
1392
|
|
|
* |
1393
|
|
|
* This is a transfer method to fool code inspection. |
1394
|
|
|
* |
1395
|
|
|
* @param int $fetchMode |
1396
|
|
|
* @return int |
1397
|
|
|
*/ |
1398
|
|
|
public function setFetchMode($fetchMode) |
1399
|
|
|
{ |
1400
|
|
|
$oldFetchMode = $this->conn->SetFetchMode($fetchMode); |
1401
|
|
|
|
1402
|
|
|
return $oldFetchMode; |
1403
|
|
|
} |
1404
|
|
|
|
1405
|
|
|
|
1406
|
|
|
/** |
1407
|
|
|
* Smart write data row(s) to db |
1408
|
|
|
* |
1409
|
|
|
* Can auto check row existence, and decide to use INSERT or UPDATE, this |
1410
|
|
|
* require primary key column included in $data array. Also, table MUST |
1411
|
|
|
* have primary key defined. |
1412
|
|
|
* |
1413
|
|
|
* Param $data can include single row(1-dim array, index is column name) |
1414
|
|
|
* or multiple rows(2-dim array, index layer 1 MUST be number and will not |
1415
|
|
|
* write to db, layer 2 is same as single row). |
1416
|
|
|
* |
1417
|
|
|
* Param $mode is case insensitive: |
1418
|
|
|
* A: auto detect, for multiple rows data, will only detect by FIRST row. |
1419
|
|
|
* U: update, |
1420
|
|
|
* I: insert, |
1421
|
|
|
* |
1422
|
|
|
* Return number of inserted or updated rows: |
1423
|
|
|
* -1: got error, |
1424
|
|
|
* N >=0: success, which N is affected rows. |
1425
|
|
|
* |
1426
|
|
|
* Even data to write exists in db and same, it will still do write |
1427
|
|
|
* operation, and been counted in return value. |
1428
|
|
|
* |
1429
|
|
|
* @param string $table |
1430
|
|
|
* @param array $data Row(s) data |
1431
|
|
|
* @param string $mode Write mode |
1432
|
|
|
* @return int |
1433
|
|
|
*/ |
1434
|
|
|
public function write($table, $data, $mode = 'A') |
1435
|
|
|
{ |
1436
|
|
|
// Find primary key column first |
1437
|
|
|
$arPk = $this->getMetaPrimaryKey($table); |
1438
|
|
|
|
1439
|
|
|
// Convert single row data to multi row mode |
1440
|
|
|
if (!isset($data[0])) { |
1441
|
|
|
$data = [0 => $data]; |
1442
|
|
|
} |
1443
|
|
|
|
1444
|
|
|
// Convert primary key to array if it's single string now |
1445
|
|
|
if (!is_array($arPk)) { |
1446
|
|
|
$arPk = [0 => $arPk]; |
1447
|
|
|
} |
1448
|
|
|
|
1449
|
|
|
// Columns in $data |
1450
|
|
|
$arCols = array_keys($data[0]); |
1451
|
|
|
// Check if primary key is assigned in $data |
1452
|
|
|
$arPkInData = true; |
1453
|
|
|
foreach ($arPk as $key) { |
1454
|
|
|
if (!in_array($key, $arCols)) { |
1455
|
|
|
$arPkInData = false; |
1456
|
|
|
} |
1457
|
|
|
} |
1458
|
|
|
// If no primary key column in $data, return -1 |
1459
|
|
|
if (false == $arPkInData) { |
|
|
|
|
1460
|
|
|
return -1; |
1461
|
|
|
} |
1462
|
|
|
|
1463
|
|
|
$mode = strtoupper($mode); |
1464
|
|
|
$sqlCfg = []; |
1465
|
|
|
|
1466
|
|
|
// Auto determine mode |
1467
|
|
View Code Duplication |
if ('A' == $mode) { |
|
|
|
|
1468
|
|
|
$where = ' WHERE '; |
1469
|
|
|
foreach ($arPk as $key) { |
1470
|
|
|
$where .= " $key = " |
1471
|
|
|
. $this->quoteValue($table, $key, $data[0][$key]) |
1472
|
|
|
. ' AND '; |
1473
|
|
|
} |
1474
|
|
|
$where = substr($where, 0, strlen($where) - 5); |
1475
|
|
|
if (0 < $this->getRowCount($table, $where)) { |
1476
|
|
|
$mode = 'U'; |
1477
|
|
|
} else { |
1478
|
|
|
$mode = 'I'; |
1479
|
|
|
} |
1480
|
|
|
} |
1481
|
|
|
|
1482
|
|
|
// Prepare sql |
1483
|
|
View Code Duplication |
if ('U' == $mode) { |
|
|
|
|
1484
|
|
|
$sqlCfg = [ |
1485
|
|
|
'UPDATE' => $table, |
1486
|
|
|
'LIMIT' => 1, |
1487
|
|
|
]; |
1488
|
|
|
// Primary key cannot change, so exclude them from SET clause, |
1489
|
|
|
// Here use prepare, actual value will assign later, do quote |
1490
|
|
|
// then. |
1491
|
|
|
// :NOTICE: Remember to put PK data to end of row data array when |
1492
|
|
|
// assign actual value, because WHERE clause is after SET clause. |
1493
|
|
|
foreach ($arPk as $key) { |
1494
|
|
|
$sqlCfg['WHERE'][] = "$key = " |
1495
|
|
|
. $this->pdoParam($table, $key); |
1496
|
|
|
unset($arCols[array_search($key, $arCols)]); |
1497
|
|
|
} |
1498
|
|
|
foreach ($arCols as $key) { |
1499
|
|
|
$sqlCfg['SET'][$key] = $this->pdoParam($table, $key); |
1500
|
|
|
} |
1501
|
|
|
|
1502
|
|
|
} elseif ('I' == $mode) { |
1503
|
|
|
$arVal = []; |
1504
|
|
|
foreach ($arCols as $key) { |
1505
|
|
|
$arVal[$key] = $this->pdoParam($table, $key); |
1506
|
|
|
} |
1507
|
|
|
$sqlCfg = [ |
1508
|
|
|
'INSERT' => $table, |
1509
|
|
|
'VALUES' => $arVal, |
1510
|
|
|
]; |
1511
|
|
|
} |
1512
|
|
|
$sql = $this->getSqlGenerator()->getPrepared($sqlCfg); |
1513
|
|
|
// @codeCoverageIgnoreStart |
1514
|
|
|
if (empty($sql)) { |
1515
|
|
|
return -1; |
1516
|
|
|
} |
1517
|
|
|
// @codeCoverageIgnoreEnd |
1518
|
|
|
|
1519
|
|
|
// Change PK position in data array |
1520
|
|
View Code Duplication |
if ('U' == $mode) { |
|
|
|
|
1521
|
|
|
foreach ($data as &$row) { |
1522
|
|
|
foreach ($arPk as $key) { |
1523
|
|
|
$v = $row[$key]; |
1524
|
|
|
unset($row[$key]); |
1525
|
|
|
$row[$key] = $v; |
1526
|
|
|
} |
1527
|
|
|
} |
1528
|
|
|
unset($row); |
1529
|
|
|
} |
1530
|
|
|
|
1531
|
|
|
// Convert data encoding |
1532
|
|
|
$this->convertEncodingSql($data); |
1533
|
|
|
|
1534
|
|
|
|
1535
|
|
|
// When use PDO, param is ? and value array must NOT be named. |
1536
|
|
|
// So consider change to use named param binding, this works better bcs |
1537
|
|
|
// does not restrict order value array. |
1538
|
|
|
// |
1539
|
|
|
// Another problem, ADOdb treat int as varchar, cause another error. |
1540
|
|
|
// @see https://bugs.php.net/bug.php?id=57655 for this bug. |
1541
|
|
|
// > All PDO_DBLIB binds are done as strings. |
1542
|
|
|
// For those column, use CONVERT(), auto done in pdoParam(). |
1543
|
|
|
if ($this->isDbPdoSybase()) { |
1544
|
|
|
foreach ($data as &$row) { |
1545
|
|
|
$keyChangedRow = []; |
1546
|
|
|
foreach ($row as $key => $val) { |
1547
|
|
|
$keyChangedRow[":$key"] = $val; |
1548
|
|
|
} |
1549
|
|
|
$row = $keyChangedRow; |
1550
|
|
|
} |
1551
|
|
|
unset($row); |
1552
|
|
|
} |
1553
|
|
|
|
1554
|
|
|
|
1555
|
|
|
// Do db prepare |
1556
|
|
|
$stmt = $this->conn->Prepare($sql); |
1557
|
|
|
|
1558
|
|
|
// @codeCoverageIgnoreStart |
1559
|
|
|
// Execute, actual write data |
1560
|
|
|
$this->conn->BeginTrans(); |
1561
|
|
|
try { |
1562
|
|
|
$this->conn->Execute($stmt, $data); |
1563
|
|
|
|
1564
|
|
|
} catch (\Exception $e) { |
1565
|
|
|
// Show error message ? |
1566
|
|
|
$this->conn->RollbackTrans(); |
1567
|
|
|
|
1568
|
|
|
return -1; |
1569
|
|
|
} |
1570
|
|
|
|
1571
|
|
|
// Any other error ? |
1572
|
|
View Code Duplication |
if (0 != $this->conn->ErrorNo()) { |
|
|
|
|
1573
|
|
|
// Log error |
1574
|
|
|
trigger_error( |
1575
|
|
|
'ErrorNo: ' . $this->conn->ErrorNo() . "\n" . |
1576
|
|
|
'ErrorMsg: ' . $this->conn->ErrorMsg(), |
1577
|
|
|
E_USER_WARNING |
1578
|
|
|
); |
1579
|
|
|
$this->conn->RollbackTrans(); |
1580
|
|
|
|
1581
|
|
|
return -1; |
1582
|
|
|
|
1583
|
|
|
} else { |
1584
|
|
|
$this->conn->CommitTrans(); |
1585
|
|
|
|
1586
|
|
|
return count($data); |
1587
|
|
|
} |
1588
|
|
|
// @codeCoverageIgnoreEnd |
1589
|
|
|
} |
1590
|
|
|
} |
1591
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.