Pdo.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  1. <?php
  2. Lib::loadClass('DBG');
  3. Lib::loadClass('Core_TypeFactory'); // Core_TypeFactory::make, Core_iPdoSqlValue::toSqlQuote
  4. class Core_Pdo extends PDO {
  5. protected $_database_name;
  6. protected $_zasob_id;
  7. protected $_type;
  8. protected $_schema;
  9. // public PDO::__construct ( string $dsn [, string $username [, string $password [, array $options ]]] )
  10. public function __construct($dsn, $username, $password, $options = array()) {
  11. $this->_database_name = $options['database'];
  12. $this->_zasob_id = $options['zasob_id'];
  13. $this->_type = $options['type'];
  14. $this->_schema = (!empty($options['schema'])) ? $options['schema'] : null;
  15. unset($options['database']);
  16. unset($options['zasob_id']);
  17. parent::__construct($dsn, $username, $password, $options);
  18. $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  19. $this->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
  20. $this->setAttribute(PDO::ATTR_EMULATE_PREPARES, 1); // Default 1, fetch returns value types: |> 0 -> int, string, null |> 1 -> string, null
  21. }
  22. public function getDatabaseName() {
  23. return $this->_database_name;
  24. }
  25. public function getZasobId() {
  26. return $this->_zasob_id;
  27. }
  28. public function getType() {
  29. return strtolower($this->_type);
  30. }
  31. public function identifierQuote($identifier) {
  32. return DB::identifierQuote($this->_type, $identifier);
  33. }
  34. public function tableNameQuote($tableName) {
  35. switch (strtolower($this->_type)) {
  36. case 'pgsql': return DB::identifierQuote($this->_type, ($this->_schema) ? "{$this->_schema}.{$tableName}" : $tableName);
  37. case 'mysql': return DB::identifierQuote($this->_type, $tableName);
  38. }
  39. return $tableName;
  40. }
  41. public function getTableStruct($tblName) {// TODO: mved to Core_Storage_*
  42. $sth = $this->prepare("
  43. -- show fields from {$tblName}
  44. select cols.COLUMN_NAME as name
  45. , cols.DATA_TYPE as type
  46. , cols.COLUMN_TYPE as type_mysql
  47. , if('YES' = cols.IS_NULLABLE, 1, 0) as is_nullable
  48. , cols.COLUMN_DEFAULT as default_value
  49. , if(cols.COLUMN_DEFAULT is null, 1, 0) as default_is_null
  50. , cols.CHARACTER_MAXIMUM_LENGTH as max_length
  51. , cols.NUMERIC_PRECISION as num_precision
  52. , cols.NUMERIC_SCALE as num_scale
  53. , cols.CHARACTER_SET_NAME as char_encoding -- latin2
  54. , cols.COLLATION_NAME as char_collation -- latin2_general_ci
  55. , cols.EXTRA as extra
  56. -- , cols.*
  57. from INFORMATION_SCHEMA.COLUMNS cols
  58. where cols.TABLE_SCHEMA = :db_name
  59. and cols.TABLE_NAME = :tbl_name
  60. ");
  61. $sth->bindValue(':db_name', $this->getDatabaseName(), PDO::PARAM_STR);
  62. $sth->bindValue(':tbl_name', $tblName, PDO::PARAM_STR);
  63. $sth->execute();
  64. $structRaw = $sth->fetchAll();
  65. if (empty($structRaw)) throw new Exception("Empty struct for table '{$tblName}'", 404);
  66. foreach ($structRaw as $field) {
  67. $struct[$field['name']] = $field;
  68. }
  69. return $struct;
  70. }
  71. public function assertTableStructXsd($tblName, $expectedStructXsd) {
  72. throw new Exception("Unimplemented - TODO!");
  73. /*
  74. - `decimal(5,2)`:
  75. <xsd:element name="A" type="decimal_5_2"/>
  76. <xsd:simpleType name="decimal_5_2">
  77. <xsd:restriction base="xsd:decimal">
  78. <xsd:totalDigits value="5"/>
  79. <xsd:fractionDigits value="2"/>
  80. </xsd:restriction>
  81. </xsd:simpleType>
  82. */
  83. /* MySQL types:
  84. int tinyint smallint mediumint bigint
  85. decimal
  86. float double real => double
  87. date datetime timestamp time year
  88. char varchar
  89. text tinytext mediumtext longtext
  90. enum
  91. set
  92. bit
  93. boolean => `tinyint(1)` -- 0 or 1
  94. serial => `bigint(20) unsigned` and unique key
  95. binary varbinary
  96. blob tinyblob mediumblob longblob
  97. geometry point linestring polygon multipoint multilinestring multipolygon geometrycollection
  98. */
  99. }
  100. /*
  101. * TODO: update keys:
  102. * TODO: keys name may be different - try to find and connect with given schema?
  103. * TODO: remove old uniq keys?
  104. */
  105. public function assertTableStruct($tblName, $expectedStruct, $params = array()) {
  106. // TODO: make backup for table?
  107. $expectedStruct = $this->_fixExpectedStruct($expectedStruct);
  108. //DBG::_(true, true, "fixedEpectedStruct", $expectedStruct, __CLASS__, __FUNCTION__, __LINE__);
  109. //DBG::_(true, true, "fixedEpectedStruct", $this->showCreateStruct($tblName, $expectedStruct, $params), __CLASS__, __FUNCTION__, __LINE__);
  110. $struct = $this->getTableStruct($tblName);
  111. $expectedFields = array();//array_keys($expectedStruct);
  112. foreach ($expectedStruct as $fldName => $fld) {
  113. if ('UNIQUE KEY' == $fld['type']) continue;
  114. if ('KEY' == $fld['type']) continue;
  115. $expectedFields[] = $fldName;
  116. }
  117. $currentFields = array_keys($struct);
  118. $missingFields = array_diff($expectedFields, $currentFields);
  119. DBG::_(true, true, "struct", $struct, __CLASS__, __FUNCTION__, __LINE__);
  120. DBG::_(true, true, "missingFields", $missingFields, __CLASS__, __FUNCTION__, __LINE__);
  121. foreach ($missingFields as $fldName) {
  122. $fld = $expectedStruct[$fldName];
  123. DBG::_(true, true, "add missing field[{$fldName}]:", $fld, __CLASS__, __FUNCTION__, __LINE__);
  124. $sqlFieldStruct = $this->showTableStructField($fldName, $fld);
  125. if ($sqlFieldStruct) {
  126. $sqlAdd = "alter table {$tblName} add {$sqlFieldStruct}";
  127. DBG::_(true, true, "sqlAdd", $sqlAdd, __CLASS__, __FUNCTION__, __LINE__);
  128. $this->exec($sqlAdd);
  129. } else {
  130. throw new Exception("Unimplemented type '{$fld['type']}': " . json_encode($fld));
  131. }
  132. }
  133. $toUpdateFields = array_intersect($expectedFields, $currentFields);
  134. DBG::_(true, true, "toUpdateFields", $toUpdateFields, __CLASS__, __FUNCTION__, __LINE__);
  135. foreach ($toUpdateFields as $fldName) {
  136. $current = $struct[$fldName];
  137. $expected = $expectedStruct[$fldName];
  138. $needChange = false;
  139. $sqlFieldStruct = $this->showTableStructField($fldName, $expected);
  140. if (!$sqlFieldStruct) throw new Exception("Unimplemented type '{$expected['type']}' for field '{$fldName}': " . json_encode($expected));
  141. $sqlChange = "alter table `{$tblName}` change `{$fldName}` {$sqlFieldStruct}";
  142. DBG::_(true, true, "DBG: sqlChange", $sqlChange, __CLASS__, __FUNCTION__, __LINE__);
  143. //DBG::_(true, true, "TODO: update field[{$fldName}]:", array('expected'=>$expected,'current'=>$current), __CLASS__, __FUNCTION__, __LINE__);
  144. if ($current['type'] != $expected['type']) {
  145. throw new Exception("Unimplemented change field type from '{$current['type']}' to '{$expected['type']}' for field '{$fldName}': " . json_encode($expected));
  146. }
  147. if ($current['is_nullable'] != $expected['is_nullable']) {
  148. $needChange = true;
  149. if ($current['is_nullable'] && !$expected['is_nullable']) {
  150. throw new Exception("Field struct needs change 'is_nullable' to false but this change is not implemented - field '{$fldName}': " . json_encode(array('expected'=>$expected,'current'=>$current)));
  151. }
  152. }
  153. if ($current['max_length'] != $expected['max_length']) {
  154. $needChange = true;
  155. if ($current['max_length'] > $expected['max_length']) {
  156. throw new Exception("Field struct needs decrease 'max_length' but this change is not implemented - field '{$fldName}': " . json_encode(array('expected'=>$expected,'current'=>$current)));
  157. }
  158. }
  159. if ($current['default_value'] !== $expected['default_value']) $needChange = true;
  160. if ($needChange) {
  161. DBG::_(true, true, "EXEC sqlChange ...", $sqlChange, __CLASS__, __FUNCTION__, __LINE__);
  162. $this->exec($sqlChange);
  163. }
  164. }
  165. }
  166. /* assert's that field struct has defined or throw exception if cannot set default value:
  167. ['type'] = 'varchar', 'int', ... (MySQL Types)
  168. ['is_nullable'] = true, false
  169. ['default_value'] = NULL or (string, numeric - based on type)
  170. default_value is not set when default_value == NULL and is_nullable == false
  171. ['default_is_null'] = true, false
  172. ['max_length'] = NULL, [0-...] // MySQL `CHARACTER_MAXIMUM_LENGTH`
  173. ['num_precision'] = NULL, [0-65]
  174. ['num_scale'] = NULL, [0-12]
  175. ['char_encoding'] = 'utf8', 'latin2', ...
  176. ['char_collation'] = 'utf8_general_ci', 'latin2_general_ci', ...
  177. ['extra'] = 'auto_increment', 'on update CURRENT_TIMESTAMP'
  178. ['values'] = null or array for enum and set
  179. TODO: validate - wrong related values. ex: {type: 'int', char_collation: 'latin2'}
  180. TODO: validate - not allowed value. ex: {type: 'xyz'}
  181. */
  182. public function _fixExpectedStruct($expectedStruct) {
  183. $fixedStruct = array();
  184. foreach ($expectedStruct as $fldName => $expected) {
  185. //DBG::_(true, true, "TODO: expected", $expected, __CLASS__, __FUNCTION__, __LINE__);
  186. if (!array_key_exists('type', $expected)) throw new Exception("Undefined type for field '{$fldName}'");
  187. if (!array_key_exists('is_nullable', $expected)) {
  188. $expected['is_nullable'] = false;
  189. if (array_key_exists('default_value', $expected) && null === $expected['default_value']) {
  190. $expected['is_nullable'] = true;
  191. }
  192. }
  193. if (!array_key_exists('default_is_null', $expected)) $expected['default_is_null'] = false;
  194. if (!array_key_exists('default_value', $expected)) {
  195. $expected['default_value'] = null;
  196. // switch ($expected['type']) {
  197. // case 'char':
  198. // case 'varchar': $expected['default_value'] = ($expected['is_nullable'])? null : ''; break;
  199. // case 'tinyint':
  200. // case 'bigint':
  201. // case 'int': $expected['default_value'] = ($expected['is_nullable'])? null : 0; break;
  202. // }
  203. if ($expected['is_nullable'] && null === $expected['default_value']) {
  204. $expected['default_is_null'] = true;
  205. }
  206. }
  207. if (!array_key_exists('max_length', $expected)) {
  208. switch ($expected['type']) {
  209. case 'char':
  210. case 'varchar': $expected['max_length'] = 255; break;
  211. case 'binary': $expected['max_length'] = 255; break;// bainary(0) is possible - why? Cannot store values.
  212. case 'varbinary': $expected['max_length'] = 255; break;
  213. case 'binary':
  214. case 'varbinary':
  215. case 'bit': throw new Exception("Undefined max_length for field '{$fldName}' with type '{$expected['type']}'");
  216. //case 'blob': $expected['max_length'] = 65535; break;// is set by engine
  217. //case 'tinyblob': $expected['max_length'] = 255; break;// is set by engine
  218. //case 'mediumblob': $expected['max_length'] = 16777215; break;// is set by engine
  219. //case 'longblob': $expected['max_length'] = 4294967295; break;// is set by engine
  220. //case 'text': $expected['max_length'] = 65535; break;// is set by engine
  221. //case 'tinytext': $expected['max_length'] = 255; break;// is set by engine
  222. //case 'mediumtext': $expected['max_length'] = 16777215; break;// is set by engine
  223. //case 'longtext': $expected['max_length'] = 4294967295; break;// is set by engine
  224. //case 'enum': $expected['max_length'] = 255; break;// is set by engine
  225. //case 'set': $expected['max_length'] = 255; break;// is set by engine
  226. default: $expected['max_length'] = null;//throw new Exception("Undefined max_length for field '{$fldName}'");
  227. }
  228. }
  229. if (!array_key_exists('num_precision', $expected)) {
  230. switch ($expected['type']) {
  231. case 'int': $expected['num_precision'] = 10; break;// int(11) - +1 in type definition
  232. case 'tinyint': $expected['num_precision'] = 3; break;// int(4)
  233. case 'smallint': $expected['num_precision'] = 5; break;// int(6)
  234. case 'mediumint': $expected['num_precision'] = 7; break;// int(8)
  235. case 'bigint': $expected['num_precision'] = 19; break;// int(20)
  236. case 'decimal': $expected['num_precision'] = 10; break;// decimal(10,0)
  237. default: $expected['num_precision'] = null;
  238. }
  239. //throw new Exception("Undefined num_precision for field '{$fldName}'");
  240. }
  241. if (!array_key_exists('num_scale', $expected)) {
  242. switch ($expected['type']) {
  243. case 'int': $expected['num_scale'] = 0; break;
  244. case 'tinyint': $expected['num_scale'] = 0; break;
  245. case 'smallint': $expected['num_scale'] = 0; break;
  246. case 'mediumint': $expected['num_scale'] = 0; break;
  247. case 'bigint': $expected['num_scale'] = 0; break;
  248. case 'decimal': $expected['num_scale'] = 0; break;// ex.: decimal(3,2); decimal(24,0)
  249. case 'double': $expected['num_scale'] = null; break;// ex.: double(10,4); double; double(17,0);
  250. case 'float': $expected['num_scale'] = null; break;// ex.: float(6,2); float; float(17,0);
  251. default: $expected['num_scale'] = null;
  252. }
  253. //throw new Exception("Undefined num_scale for field '{$fldName}'");
  254. }
  255. if (!array_key_exists('char_encoding', $expected)) {
  256. switch ($expected['type']) {
  257. case 'char':
  258. case 'varchar': $expected['char_encoding'] = 'utf8'; break;
  259. case 'enum':
  260. case 'set': $expected['char_encoding'] = 'utf8'; break;
  261. case 'text':
  262. case 'tinytext':
  263. case 'mediumtext':
  264. case 'longtext': $expected['char_encoding'] = 'utf8'; break;
  265. default: $expected['char_encoding'] = null;
  266. }
  267. //throw new Exception("Undefined char_encoding for field '{$fldName}'");
  268. }
  269. if (!array_key_exists('char_collation', $expected)) {
  270. switch ($expected['type']) {
  271. case 'char':
  272. case 'varchar': $expected['char_collation'] = 'utf8_general_ci'; break;
  273. case 'enum':
  274. case 'set': $expected['char_collation'] = 'utf8_general_ci'; break;
  275. case 'text':
  276. case 'tinytext':
  277. case 'mediumtext':
  278. case 'longtext': $expected['char_collation'] = 'utf8_general_ci'; break;
  279. default: $expected['char_collation'] = null;
  280. }
  281. //throw new Exception("Undefined char_collation for field '{$fldName}'");
  282. }
  283. if (!array_key_exists('extra', $expected)) {
  284. $expected['extra'] = null;//throw new Exception("Undefined extra for field '{$fldName}'");
  285. }
  286. if (!array_key_exists('values', $expected) || !is_array($expected['values']) || empty($expected['values'])) {
  287. switch ($expected['type']) {
  288. case 'enum':
  289. case 'set': throw new Exception("Undefined values for field '{$fldName}' with type '{$expected['type']}'");
  290. default: $expected['values'] = null;
  291. }
  292. }
  293. $fixedStruct[$fldName] = $expected;
  294. }
  295. return $fixedStruct;
  296. }
  297. public function showCreateStruct($tblName, $struct, $params = array()) {
  298. // TODO: check database type, if == MySQL - fix construct
  299. $expectedStruct = $this->_fixExpectedStruct($struct);
  300. $linesSql = array();
  301. $dbgSql = array();
  302. foreach ($expectedStruct as $fldName => $fld) {
  303. $sqlFieldStruct = $this->showTableStructField($fldName, $fld);
  304. if ($sqlFieldStruct) {
  305. $linesSql[] = $sqlFieldStruct;
  306. } else {
  307. $dbgSql[] = "-- Unimplemented type '{$fld['type']}': " . json_encode($fld);
  308. }
  309. }
  310. $linesSql = implode("\n\t\t, ", $linesSql);
  311. $dbgSql = implode("\n\t\t", $dbgSql);
  312. $tblCharEncoding = V::get('char_encoding', 'utf8', $params);
  313. $structSql = <<<EOF_STRUCT_MYSQL
  314. CREATE TABLE IF NOT EXISTS `{$tblName}` (
  315. {$linesSql}
  316. {$dbgSql}
  317. ) ENGINE=MyISAM DEFAULT CHARSET={$tblCharEncoding}
  318. EOF_STRUCT_MYSQL;
  319. return $structSql;
  320. }
  321. public function showTableStructField($fldName, $fld) {
  322. // TODO: check database type, if == MySQL - fix construct
  323. $nullSql = ($fld['is_nullable'])? '' : 'NOT NULL';
  324. $defaultSql = (is_null($fld['default_value']))? 'DEFAULT NULL' : "DEFAULT '{$fld['default_value']}'";
  325. if (is_null($fld['default_value']) && !$fld['is_nullable']) $defaultSql = '';
  326. switch ($fld['type']) {
  327. case 'char':
  328. case 'varchar': return "`{$fldName}` {$fld['type']}({$fld['max_length']}) {$nullSql} {$defaultSql}"; break;
  329. case 'text':
  330. case 'tinytext':
  331. case 'longtext':
  332. case 'mediumtext': return "`{$fldName}` {$fld['type']} {$nullSql}"; break;
  333. case 'time':
  334. case 'timestamp':
  335. case 'year':
  336. case 'date':
  337. case 'datetime': return "`{$fldName}` {$fld['type']} {$nullSql} {$defaultSql}"; break;
  338. // -- Unimplemented type 'int': {"type":"int","is_nullable":false,"default_is_null":false,"default_value":null,"max_length":null,"num_precision":10,"num_scale":0,"char_encoding":null,"char_collation":null,"extra":null}
  339. // -- Unimplemented type 'int': {"type":"int","num_precision":11,"default_value":null,"is_nullable":true,"default_is_null":false,"max_length":null,"num_scale":0,"char_encoding":null,"char_collation":null,"extra":null}
  340. case 'int':
  341. case 'tinyint':
  342. case 'smallint':
  343. case 'mediumint':
  344. case 'bigint':
  345. if ($fld['num_precision'] > 0) {
  346. $typeParamsSql = "(" . ($fld['num_precision'] + 1) . ")";
  347. }
  348. //if ($fld['num_scale']) $typeParamsSql = ",{$fld['num_scale']}";
  349. return "`{$fldName}` {$fld['type']}{$typeParamsSql} {$nullSql} {$defaultSql}";
  350. break;
  351. case 'decimal':
  352. $typeParamsSql = "{$fld['num_precision']},{$fld['num_scale']}";
  353. return "`{$fldName}` {$fld['type']}({$typeParamsSql}) {$nullSql} {$defaultSql}";
  354. break;
  355. case 'float':
  356. case 'double':
  357. case 'real':
  358. return "`{$fldName}` {$fld['type']} {$nullSql} {$defaultSql}";
  359. break;
  360. case 'enum':
  361. case 'set':
  362. $typeParamsSql = "'" . implode("','", $fld['values']) . "'";
  363. return "`{$fldName}` {$fld['type']}({$typeParamsSql}) {$nullSql} {$defaultSql}";
  364. break;
  365. case 'bit':
  366. case 'binary':
  367. case 'varbinary':
  368. return "`{$fldName}` {$fld['type']}({$fld['max_length']}) {$nullSql} {$defaultSql}";
  369. break;
  370. case 'boolean':
  371. case 'serial':
  372. case 'blob':
  373. case 'tinyblob':
  374. case 'mediumblob':
  375. case 'longblob':
  376. return "`{$fldName}` {$fld['type']} {$nullSql} {$defaultSql}";
  377. break;
  378. case 'geometry':
  379. case 'point':
  380. case 'linestring':
  381. case 'polygon':
  382. case 'multipoint':
  383. case 'multilinestring':
  384. case 'multipolygon':
  385. case 'geometrycollection':
  386. return "`{$fldName}` {$fld['type']} {$nullSql} {$defaultSql}";
  387. break;
  388. case 'UNIQUE KEY':
  389. $keyFieldsSql = "`" . implode("`,`", $fld['key_fields']) . "`";
  390. return "UNIQUE KEY `{$fldName}` ({$keyFieldsSql})";
  391. break;
  392. case 'KEY':
  393. $keyFieldsSql = "`" . implode("`,`", $fld['key_fields']) . "`";
  394. return "KEY `{$fldName}` ({$keyFieldsSql})";
  395. break;
  396. case 'PRIMARY KEY':
  397. $keyFieldsSql = "`" . implode("`,`", $fld['key_fields']) . "`";
  398. return "PRIMARY KEY ({$keyFieldsSql})";
  399. break;
  400. }
  401. return null;
  402. }
  403. public function fetchValue($sql, $values = []) { // for sql like `select count() from ...`
  404. $sth = $this->prepare($sql);
  405. if (!empty($values)) {
  406. $this->bindValues($sth, $values);
  407. DBG::log($this->getRawSql($sth), 'sql');
  408. } else {
  409. DBG::log($sql, 'sql');
  410. }
  411. $sth->execute();
  412. return $sth->fetchColumn();
  413. }
  414. public function fetchValuesList($sql, $values = []) { // for sql like `select ID from ...` @returns array of ID
  415. return array_map(function ($row) {
  416. return reset($row);
  417. }, $this->fetchAll($sql, $values));
  418. }
  419. public function fetchValuesListByKey($sql, $key, $values = []) { // for sql like `select ID from ...` @returns array of ID
  420. return array_map(function ($row) {
  421. return reset($row);
  422. }, $this->fetchAllByKey($sql, $key, $values));
  423. }
  424. public function fetchKeyValueList($sql, $keyField, $valueField, $values = []) { // for sql like `select key, val from ...` @returns assoc array key => val
  425. return array_map(function ($row) use ($valueField) {
  426. return V::get($valueField, '', $row);
  427. }, $this->fetchAllByKey($sql, $keyField, $values));
  428. }
  429. public function fetchFirst($sql, $values = []) { // fetch only first row
  430. $sth = $this->prepare($sql);
  431. if (!empty($values)) {
  432. $this->bindValues($sth, $values);
  433. DBG::log($this->getRawSql($sth), 'sql');
  434. } else {
  435. DBG::log($sql, 'sql');
  436. }
  437. $sth->execute();
  438. return $sth->fetch();
  439. }
  440. public function fetchFirstAsObject($sql, $values = []) { // fetch only first row as object or null - used in old functions
  441. $item = $this->fetchFirst($sql, $values);
  442. return ($item) ? (object)$item : null;
  443. }
  444. public function fetchFirstNoLog($sql, $values = []) { // fetch only first row - used in User::getID() required in DBG
  445. $sth = $this->prepare($sql);
  446. if (!empty($values)) {
  447. $this->bindValues($sth, $values);
  448. }
  449. $sth->execute();
  450. return $sth->fetch();
  451. }
  452. public function fetchAll($sql, $values = []) {
  453. $sth = $this->prepare($sql);
  454. if (!empty($values)) {
  455. $this->bindValues($sth, $values);
  456. DBG::log($this->getRawSql($sth), 'sql');
  457. } else {
  458. DBG::log($sql, 'sql');
  459. }
  460. $sth->execute();
  461. return $sth->fetchAll();
  462. }
  463. public function fetchAllByKey($sql, $key = 'ID', $values = []) {
  464. $rowsByKey = array();
  465. $sth = $this->prepare($sql);
  466. if (!empty($values)) {
  467. $this->bindValues($sth, $values);
  468. DBG::log($this->getRawSql($sth), 'sql');
  469. } else {
  470. DBG::log($sql, 'sql');
  471. }
  472. $sth->execute();
  473. $rows = $sth->fetchAll();
  474. foreach ($rows as $row) {
  475. $keyRow = V::get($key, null, $row);
  476. $rowsByKey[$keyRow] = $row;
  477. }
  478. return $rowsByKey;
  479. }
  480. public function bindValues($sth, $values) {
  481. foreach ($values as $name => $value) {
  482. $val = $value;
  483. $type = PDO::PARAM_STR;
  484. if (is_array($value)) {
  485. $val = $value[0];
  486. if (count($value) > 1) {
  487. $type = $value[1];
  488. }
  489. }
  490. $sth->bindValue($name, $val, $type);
  491. if (!isset($sth->bindedValues)) $sth->bindedValues = array();
  492. $sth->bindedValues[$name] = array($val, $type);
  493. }
  494. }
  495. public function getRawSql($sth, $values = array()) {
  496. $sql = $sth->queryString;
  497. $params = array();
  498. if (!empty($sth->bindedValues)) {
  499. foreach ($sth->bindedValues as $name => $value) {
  500. $params[$name] = array($value[0], $value[1]);
  501. }
  502. }
  503. foreach ($values as $name => $value) {
  504. $val = $value;
  505. $type = PDO::PARAM_STR;
  506. if (is_array($value)) {
  507. $val = $value[0];
  508. if (count($value) > 1) {
  509. $type = $value[1];
  510. }
  511. }
  512. $params[$name] = array($val, $type);
  513. }
  514. if (!empty($params)) {
  515. foreach ($params as $name => $val) {
  516. $outValue = $val[0];
  517. if (PDO::PARAM_STR == $val[1]) $outValue = "'{$outValue}'";
  518. $sql = str_replace((':' === $name[0] ? $name : ":{$name}"), $outValue, $sql);
  519. }
  520. }
  521. return $sql;
  522. }
  523. public function insert($tableName, $item, $sqlSchema = []) {// @returns int last inserted id
  524. if (empty($tableName)) throw new Exception("Missing table name");
  525. if (!is_array($item)) throw new Exception("Missing item");
  526. $sqlListFields = [];
  527. $sqlListValues = [];
  528. foreach ($item as $field => $val) {
  529. $sqlListFields[] = $this->identifierQuote($field);
  530. $sqlListValues[] = $this->convertValueToSqlSafe($val, V::get($field, null, $sqlSchema));
  531. }
  532. $sqlTableName = $this->tableNameQuote($tableName);
  533. $sql = "
  534. insert into {$sqlTableName} (" . implode(", ", $sqlListFields) . ")
  535. values (" . implode(", ", $sqlListValues) . ")
  536. ";
  537. $this->execSql($sql);
  538. return $this->lastInsertId();
  539. }
  540. public function insertIgnore($tableName, $item, $sqlSchema = []) {// @returns int last inserted id
  541. if (empty($tableName)) throw new Exception("Missing table name");
  542. if (!is_array($item)) throw new Exception("Missing item");
  543. $sqlListFields = [];
  544. $sqlListValues = [];
  545. foreach ($item as $field => $val) {
  546. $sqlListFields[] = $this->identifierQuote($field);
  547. $sqlListValues[] = $this->convertValueToSqlSafe($val, V::get($field, null, $sqlSchema));
  548. }
  549. $sqlTableName = $this->tableNameQuote($tableName);
  550. $sql = "
  551. insert ignore into {$sqlTableName} (" . implode(", ", $sqlListFields) . ")
  552. values (" . implode(", ", $sqlListValues) . ")
  553. ";
  554. $this->execSql($sql);
  555. return $this->lastInsertId();
  556. }
  557. public function update($tableName, $primaryKeyName, $primaryKey, $item, $sqlSchema = []) {// @returns int affected rows
  558. if (empty($tableName)) throw new Exception("Missing table name");
  559. if (empty($primaryKeyName)) throw new Exception("Missing primaryKey name");
  560. if (empty($primaryKey)) throw new Exception("Missing primaryKey");
  561. if (empty($item) || !is_array($item)) throw new Exception("Missing item");
  562. $sqlUpdateSet = [];
  563. foreach ($item as $field => $val) {
  564. $sqlFieldName = $this->identifierQuote($field);
  565. $sqlValue = $this->convertValueToSqlSafe($val, V::get($field, null, $sqlSchema));
  566. $sqlUpdateSet[] = "{$sqlFieldName} = {$sqlValue}";
  567. }
  568. $sqlTableName = $this->tableNameQuote($tableName);
  569. $sqlPkName = $this->identifierQuote($primaryKeyName);
  570. $sqlPkValue = $this->quote($primaryKey, PDO::PARAM_STR);
  571. $sql = "
  572. update {$sqlTableName}
  573. set " . implode("\n , ", $sqlUpdateSet) . "
  574. where {$sqlPkName} = {$sqlPkValue}
  575. ";
  576. return $this->execSql($sql);
  577. }
  578. // fieldName => fieldValue, // will update if duplicate key (sql: `on duplicate key update`)
  579. // '@insert' => [
  580. // 'A_RECORD_CREATE_AUTHOR' => User::getLogin(),
  581. // 'A_RECORD_CREATE_DATE' => 'NOW()',
  582. // ],
  583. // '@update' => [
  584. // 'A_RECORD_UPDATE_AUTHOR' => User::getLogin(),
  585. // 'A_RECORD_UPDATE_DATE' => 'NOW()',
  586. // ]
  587. public function insertOrUpdate($tableName, $item, $sqlSchema = []) {
  588. if (empty($tableName)) throw new Exception("Missing table name");
  589. if (empty($item) || !is_array($item)) throw new Exception("Missing item");
  590. $sqlListFields = [];
  591. $sqlListValues = [];
  592. $sqlUpdateSet = [];
  593. foreach ($item as $field => $val) {
  594. if ('@insert' == $field) continue;
  595. if ('@update' == $field) continue;
  596. $sqlFieldName = $this->identifierQuote($field);
  597. $sqlValue = $this->convertValueToSqlSafe($val, V::get($field, null, $sqlSchema));
  598. $sqlListFields[] = $sqlFieldName;
  599. $sqlListValues[] = $sqlValue;
  600. $sqlUpdateSet[] = "{$sqlFieldName} = {$sqlValue}";
  601. }
  602. if (!empty($item['@insert'])) {
  603. foreach ($item['@insert'] as $field => $val) {
  604. $sqlFieldName = $this->identifierQuote($field);
  605. $sqlValue = $this->convertValueToSqlSafe($val, V::get($field, null, $sqlSchema));
  606. $sqlListFields[] = $sqlFieldName;
  607. $sqlListValues[] = $sqlValue;
  608. }
  609. }
  610. if (!empty($item['@update'])) {
  611. foreach ($item['@update'] as $field => $val) {
  612. $sqlFieldName = $this->identifierQuote($field);
  613. $sqlValue = $this->convertValueToSqlSafe($val, V::get($field, null, $sqlSchema));
  614. $sqlUpdateSet[] = "{$sqlFieldName} = {$sqlValue}";
  615. }
  616. }
  617. $sqlTableName = $this->tableNameQuote($tableName);
  618. $sql = "
  619. insert into {$sqlTableName} (" . implode(", ", $sqlListFields) . ")
  620. values (" . implode(", ", $sqlListValues) . ")
  621. ";
  622. if (!empty($sqlUpdateSet)) $sql .= " on duplicate key update " . implode(", ", $sqlUpdateSet);
  623. $affected = $this->execSql($sql);
  624. return true; // return $affected; // $this->lastInsertId();
  625. }
  626. public function convertValueToSqlSafe($value, $xsdType = null) {
  627. if (is_object($value)) {
  628. if ($value instanceof Core_iPdoSqlValue) return $value->toSqlQuote($this); // TODO: use P5Type to convert to sql value
  629. // Example: $value = DB::makeValue($type = 'FROM_UNIXTIME', $value = 1262304000, $params);
  630. throw new Exception("DB Error: convertValueToSqlSafe value not implementing iPdoSqlValue '" . get_class($value) . "'");
  631. }
  632. if ('NOW()' === $value) return 'NOW()';
  633. if (NULL === $value) return 'NULL';
  634. if ('GeomFromText' == substr($value, 0, strlen('GeomFromText'))) return $value; // TODO: convert to Core_iPdoSqlValue, security
  635. if ('FROM_UNIXTIME' == substr($value, 0, strlen('FROM_UNIXTIME'))) return $value; // TODO: convert to Core_iPdoSqlValue, security
  636. return $this->quote($value, PDO::PARAM_STR); // TODO: use $sqlSchema if set
  637. }
  638. public function execSql($sql, $values = []) {
  639. try {
  640. if (empty($values)) {
  641. DBG::log($sql, 'sql');
  642. $retAffected = $this->exec($sql);
  643. } else {
  644. $sth = $this->prepare($sql);
  645. if (!empty($values)) {
  646. $this->bindValues($sth, $values);
  647. DBG::log($this->getRawSql($sth), 'sql');
  648. } else {
  649. DBG::log($sql, 'sql');
  650. }
  651. $sth->execute();
  652. $retAffected = $sth->rowCount();
  653. }
  654. } catch (Exception $e) {
  655. DBG::log($e);
  656. $dbType = $this->getType();
  657. $duplicateRegexp = "/^SQLSTATE\[23000\]\: Integrity constraint violation: 1062 Duplicate entry '([0-9]+)' for key '(.*)'/";
  658. if ('mysql' == $dbType && preg_match_all($duplicateRegexp, $e->getMessage(), $matches) > 0) {
  659. DBG::log(['matches'=>$matches,'msg'=>$e->getMessage(),'regex'=>$duplicateRegexp], 'array', '$matches duplicate test');
  660. throw new MysqlDuplicateEntryException("Duplicate entry '{$matches[1][0]}'", 1062, null, $matches[1][0], $matches[2][0]);
  661. } else {
  662. throw $e;
  663. }
  664. }
  665. return $retAffected;
  666. }
  667. public function getBlob($tableName, $fieldName, $pkField, $primaryKey) {
  668. if (empty($tableName)) throw new Exception("Missing tableName in PDO::getBlob");
  669. if (empty($fieldName)) throw new Exception("Missing fieldName in PDO::getBlob");
  670. if (empty($pkField)) throw new Exception("Missing pkField in PDO::getBlob");
  671. if (empty($primaryKey)) throw new Exception("Missing primaryKey in PDO::getBlob");
  672. $dbType = $this->getType();
  673. switch ($dbType) {
  674. case 'mysql': {
  675. $sql = "
  676. select `{$fieldName}`
  677. from `{$tableName}`
  678. where `{$pkField}` = :pk
  679. limit 1
  680. ";
  681. $sth = $this->prepare($sql);
  682. $sth->bindValue(':pk', $primaryKey, PDO::PARAM_STR);
  683. $sth->execute();
  684. $sth->bindColumn(1, $content, PDO::PARAM_LOB);
  685. $sth->fetch();
  686. return $content;
  687. } break;
  688. default: throw new Exception("Not implemented getBlob for database type '{$dbType}'");
  689. }
  690. }
  691. public function queryNotBuffered($query) {
  692. if ($bufferedQuery = $this->getAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY)) DB::getPDO()->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
  693. $return = parent::query($query);
  694. if ($bufferedQuery) DB::getPDO()->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
  695. return $return;
  696. }
  697. public function tryHandleException($handler, $methodName, $args) { // try again on exception
  698. try {
  699. return call_user_func_array([ $this, $methodName ], $args);
  700. } catch (Exception $e) {
  701. DBG::log("DBG:PDO->tryHandleException Exception trying to fix using handler ...");
  702. DBG::log($e);
  703. $handler($e);
  704. return call_user_func_array([ $this, $methodName ], $args);
  705. }
  706. }
  707. }
  708. class MysqlDuplicateEntryException extends DatabaseDuplicateEntryException {}
  709. class DatabaseDuplicateEntryException extends Exception {
  710. public function __construct($message, $code = 0, Exception $previous = null, $sqlPrimaryKey = '', $sqlKeyName = '') {
  711. $this->keyName = $sqlKeyName;
  712. $this->primaryKey = $sqlPrimaryKey;
  713. parent::__construct($message, $code, $previous);
  714. }
  715. }