1<?php
2/*
3** Zabbix
4** Copyright (C) 2001-2021 Zabbix SIA
5**
6** This program is free software; you can redistribute it and/or modify
7** it under the terms of the GNU General Public License as published by
8** the Free Software Foundation; either version 2 of the License, or
9** (at your option) any later version.
10**
11** This program is distributed in the hope that it will be useful,
12** but WITHOUT ANY WARRANTY; without even the implied warranty of
13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14** GNU General Public License for more details.
15**
16** You should have received a copy of the GNU General Public License
17** along with this program; if not, write to the Free Software
18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19**/
20
21
22class DB {
23
24	const SCHEMA_FILE = 'schema.inc.php';
25
26	const DBEXECUTE_ERROR = 1;
27	const RESERVEIDS_ERROR = 2;
28	const SCHEMA_ERROR = 3;
29	const INPUT_ERROR = 4;
30
31	const TABLE_TYPE_CONFIG = 1;
32	const TABLE_TYPE_HISTORY = 2;
33
34	const FIELD_TYPE_INT = 'int';
35	const FIELD_TYPE_CHAR = 'char';
36	const FIELD_TYPE_ID = 'id';
37	const FIELD_TYPE_FLOAT = 'float';
38	const FIELD_TYPE_UINT = 'uint';
39	const FIELD_TYPE_BLOB = 'blob';
40	const FIELD_TYPE_TEXT = 'text';
41
42	private static $schema = null;
43
44	/**
45	 * @var DbBackend
46	 */
47	private static $dbBackend;
48
49	/**
50	 * Get necessary DB class.
51	 *
52	 * @return DbBackend
53	 */
54	public static function getDbBackend() {
55		global $DB;
56
57		if (!self::$dbBackend) {
58			switch ($DB['TYPE']) {
59				case ZBX_DB_MYSQL:
60					self::$dbBackend = new MysqlDbBackend();
61					break;
62				case ZBX_DB_POSTGRESQL:
63					self::$dbBackend = new PostgresqlDbBackend();
64					break;
65				case ZBX_DB_ORACLE:
66					self::$dbBackend = new OracleDbBackend();
67					break;
68				case ZBX_DB_DB2:
69					self::$dbBackend = new Db2DbBackend();
70					break;
71				case ZBX_DB_SQLITE3:
72					self::$dbBackend = new SqliteDbBackend();
73					break;
74			}
75		}
76
77		return self::$dbBackend;
78	}
79
80	private static function exception($code, $error) {
81		throw new DBException($error, $code);
82	}
83
84	/**
85	 * Reserve ids for primary key of passed table.
86	 * If record for table does not exist or value is out of range, ids record is created
87	 * using maximum id from table or minimum allowed value.
88	 *
89	 * @throw APIException
90	 *
91	 * @static
92	 *
93	 * @param string $table table name
94	 * @param int $count number of ids to reserve
95	 *
96	 * @return string
97	 */
98	protected static function reserveIds($table, $count) {
99		global $DB;
100
101		$tableSchema = self::getSchema($table);
102		$id_name = $tableSchema['key'];
103
104		$sql = 'SELECT nextid'.
105				' FROM ids'.
106				' WHERE table_name='.zbx_dbstr($table).
107					' AND field_name='.zbx_dbstr($id_name);
108
109		// SQLite3 does not support this syntax. Since we are in transaction, it can be ignored.
110		if ($DB['TYPE'] != ZBX_DB_SQLITE3) {
111			$sql = $sql.' FOR UPDATE';
112		}
113
114		$res = DBfetch(DBselect($sql));
115
116		if ($res) {
117			$maxNextId = bcadd($res['nextid'], $count, 0);
118
119			if (bccomp($maxNextId, ZBX_DB_MAX_ID) == 1) {
120				$nextid = self::refreshIds($table, $count);
121			}
122			else {
123				$sql = 'UPDATE ids'.
124						' SET nextid='.$maxNextId.
125						' WHERE table_name='.zbx_dbstr($table).
126							' AND field_name='.zbx_dbstr($id_name);
127
128				if (!DBexecute($sql)) {
129					self::exception(self::DBEXECUTE_ERROR, 'DBEXECUTE_ERROR');
130				}
131
132				$nextid = bcadd($res['nextid'], 1, 0);
133			}
134		}
135
136		/*
137		 * Detect either the query is executable at all? If query is valid and schema is correct but query still cannot
138		 * be executed, then there is a good chance that previous transaction has left row level lock unreleased or it
139		 * is still running. In such a case execution must be stopped, otherwise it will call self::refreshIds method.
140		 */
141		elseif (!DBexecute($sql)) {
142			self::exception(self::DBEXECUTE_ERROR,
143				_('Your database is not working properly. Please wait a few minutes and try to repeat this action. If the problem still persists, please contact system administrator. The problem might be caused by long running transaction or row level lock accomplished by your database management system.')
144			);
145		}
146		// If query is executable, but still returns false, only then call refreshIds.
147		else {
148			$nextid = self::refreshIds($table, $count);
149		}
150
151		return $nextid;
152	}
153
154	/**
155	 * Refresh id record for given table.
156	 * Record is deleted and then created again with value of maximum id from table or minimum allowed.
157	 *
158	 * @throw APIException
159	 *
160	 * @static
161	 *
162	 * @param string $table table name
163	 * @param int    $count number of ids to reserve
164	 *
165	 * @return string
166	 */
167	private static function refreshIds($table, $count) {
168		$tableSchema = self::getSchema($table);
169		$id_name = $tableSchema['key'];
170
171		// when we reach the maximum ID, we try to refresh them to check if any IDs have been freed
172		$sql = 'DELETE FROM ids WHERE table_name='.zbx_dbstr($table).' AND field_name='.zbx_dbstr($id_name);
173
174		if (!DBexecute($sql)) {
175			self::exception(self::DBEXECUTE_ERROR, 'DBEXECUTE_ERROR');
176		}
177
178		$row = DBfetch(DBselect('SELECT MAX('.$id_name.') AS id FROM '.$table));
179
180		$nextid = ($row && $row['id']) ? $row['id'] : 0;
181
182		$maxNextId = bcadd($nextid, $count, 0);
183
184		if (bccomp($maxNextId, ZBX_DB_MAX_ID) == 1) {
185			self::exception(
186				self::RESERVEIDS_ERROR, __METHOD__.' ID greater than maximum allowed for table "'.$table.'"'
187			);
188		}
189
190		$sql = 'INSERT INTO ids (table_name,field_name,nextid)'.
191				' VALUES ('.zbx_dbstr($table).','.zbx_dbstr($id_name).','.$maxNextId.')';
192
193		if (!DBexecute($sql)) {
194			self::exception(self::DBEXECUTE_ERROR, 'DBEXECUTE_ERROR');
195		}
196
197		$nextid = bcadd($nextid, 1, 0);
198
199		return $nextid;
200	}
201
202	/**
203	 * Returns the array describing the database schema.
204	 *
205	 * If the $table parameter is passed, the method will return the schema for the given table,
206	 * otherwise - for the whole database.
207	 *
208	 * @static
209	 *
210	 * @throws APIException if the given table does not exist
211	 *
212	 * @param string $table
213	 *
214	 * @return array
215	 */
216	public static function getSchema($table = null) {
217		if (is_null(self::$schema)) {
218			self::$schema = include(dirname(__FILE__).'/../../'.self::SCHEMA_FILE);
219		}
220
221		if (is_null($table)) {
222			return self::$schema;
223		}
224		elseif (isset(self::$schema[$table])) {
225			return self::$schema[$table];
226		}
227		else {
228			self::exception(self::SCHEMA_ERROR, _s('Table "%1$s" does not exist.', $table));
229		}
230	}
231
232	/**
233	 * Returns the names of the fields that are used as the primary key of the table.
234	 *
235	 * @static
236	 *
237	 * @param string $tableName
238	 *
239	 * @return string|array
240	 */
241	protected static function getPk($tableName) {
242		$schema = self::getSchema($tableName);
243
244		return $schema['key'];
245	}
246
247	/**
248	 * Returns true if the table $tableName has the $fieldName field.
249	 *
250	 * @static
251	 *
252	 * @param string $tableName
253	 * @param string $fieldName
254	 *
255	 * @return bool
256	 */
257	public static function hasField($tableName, $fieldName) {
258		$schema = self::getSchema($tableName);
259
260		return isset($schema['fields'][$fieldName]);
261	}
262
263	private static function addMissingFields($tableSchema, $values) {
264		global $DB;
265
266		if ($DB['TYPE'] == ZBX_DB_MYSQL) {
267			foreach ($tableSchema['fields'] as $name => $field) {
268				if ($field['type'] == DB::FIELD_TYPE_TEXT && !$field['null']) {
269					foreach ($values as &$value) {
270						if (!isset($value[$name])) {
271							$value[$name] = '';
272						}
273					}
274					unset($value);
275				}
276			}
277		}
278
279		return $values;
280	}
281
282	public static function getDefaults($table) {
283		$table = self::getSchema($table);
284
285		$defaults = [];
286		foreach ($table['fields'] as $name => $field) {
287			if (isset($field['default'])) {
288				$defaults[$name] = $field['default'];
289			}
290		}
291		return $defaults;
292	}
293
294	/**
295	 * Returns the default value of the given field.
296	 *
297	 * @param string $table		name of the table
298	 * @param string $field		name of the field
299	 *
300	 * @return string|null
301	 */
302	public static function getDefault($table, $field) {
303		$table = self::getSchema($table);
304		$field = $table['fields'][$field];
305
306		return isset($field['default']) ? $field['default'] : null;
307	}
308
309	public static function checkValueTypes($table, &$values) {
310		global $DB;
311		$tableSchema = self::getSchema($table);
312
313		foreach ($values as $field => $value) {
314			if (!isset($tableSchema['fields'][$field])) {
315				unset($values[$field]);
316				continue;
317			}
318
319			if (isset($tableSchema['fields'][$field]['ref_table'])) {
320				if ($tableSchema['fields'][$field]['null']) {
321					$values[$field] = ($values[$field] == '0') ? NULL : $values[$field];
322				}
323			}
324
325			if (is_null($values[$field])) {
326				if ($tableSchema['fields'][$field]['null']) {
327					$values[$field] = 'NULL';
328				}
329				elseif (isset($tableSchema['fields'][$field]['default'])) {
330					$values[$field] = zbx_dbstr($tableSchema['fields'][$field]['default']);
331				}
332				else {
333					self::exception(self::DBEXECUTE_ERROR,
334						_s('Field "%1$s" cannot be set to NULL.', $field)
335					);
336				}
337			}
338			else {
339				switch ($tableSchema['fields'][$field]['type']) {
340					case self::FIELD_TYPE_CHAR:
341						$length = mb_strlen($values[$field]);
342
343						if ($length > $tableSchema['fields'][$field]['length']) {
344							self::exception(self::SCHEMA_ERROR, _s('Value "%1$s" is too long for field "%2$s" - %3$d characters. Allowed length is %4$d characters.',
345								$values[$field], $field, $length, $tableSchema['fields'][$field]['length']));
346						}
347						$values[$field] = zbx_dbstr($values[$field]);
348						break;
349					case self::FIELD_TYPE_ID:
350					case self::FIELD_TYPE_UINT:
351						if (!zbx_ctype_digit($values[$field])) {
352							self::exception(self::DBEXECUTE_ERROR, _s('Incorrect value "%1$s" for unsigned int field "%2$s".', $values[$field], $field));
353						}
354						$values[$field] = zbx_dbstr($values[$field]);
355						break;
356					case self::FIELD_TYPE_INT:
357						if (!zbx_is_int($values[$field])) {
358							self::exception(self::DBEXECUTE_ERROR, _s('Incorrect value "%1$s" for int field "%2$s".', $values[$field], $field));
359						}
360						$values[$field] = zbx_dbstr($values[$field]);
361						break;
362					case self::FIELD_TYPE_FLOAT:
363						if (!is_numeric($values[$field])) {
364							self::exception(self::DBEXECUTE_ERROR, _s('Incorrect value "%1$s" for float field "%2$s".', $values[$field], $field));
365						}
366						$values[$field] = zbx_dbstr($values[$field]);
367						break;
368					case self::FIELD_TYPE_TEXT:
369						if ($DB['TYPE'] == ZBX_DB_DB2 || $DB['TYPE'] == ZBX_DB_ORACLE) {
370							$length = mb_strlen($values[$field]);
371
372							if ($length > 2048) {
373								self::exception(self::SCHEMA_ERROR, _s('Value "%1$s" is too long for field "%2$s" - %3$d characters. Allowed length is 2048 characters.',
374									$values[$field], $field, $length));
375							}
376						}
377						$values[$field] = zbx_dbstr($values[$field]);
378						break;
379				}
380			}
381		}
382	}
383
384	/**
385	 * Returns the records that match the given criteria.
386	 *
387	 * @static
388	 *
389	 * @param string $tableName
390	 * @param array $criteria   An associative array of field-value pairs, where value can be either a single value
391	 *                          or an array (IN)
392	 *
393	 * @return array
394	 */
395	public static function find($tableName, array $criteria = []) {
396		// build the WHERE part
397		$sqlWhere = [];
398		foreach ($criteria as $field => $value) {
399			// check if the table has this field
400			if (!self::hasField($tableName, $field)) {
401				self::exception(self::DBEXECUTE_ERROR, _s('Table "%1$s" doesn\'t have a field named "%2$s".', $tableName, $field));
402			}
403
404			$sqlWhere[] = dbConditionString($field, zbx_toArray($value));
405		}
406
407		// build query
408		$sql = 'SELECT * FROM '.$tableName;
409		if ($sqlWhere) {
410			$sql .= ' WHERE '.implode(' AND ', $sqlWhere);
411		}
412
413		return DBfetchArray(DBSelect($sql));
414	}
415
416	/**
417	 * Insert data into DB.
418	 *
419	 * @param string $table
420	 * @param array  $values pair of fieldname => fieldvalue
421	 * @param bool   $getids
422	 *
423	 * @return array    an array of ids with the keys preserved
424	 */
425	public static function insert($table, $values, $getids = true) {
426		if (empty($values)) {
427			return true;
428		}
429
430		$resultIds = [];
431
432		if ($getids) {
433			$id = self::reserveIds($table, count($values));
434		}
435
436		$tableSchema = self::getSchema($table);
437
438		$values = self::addMissingFields($tableSchema, $values);
439
440		foreach ($values as $key => $row) {
441			if ($getids) {
442				$resultIds[$key] = $id;
443				$row[$tableSchema['key']] = $id;
444				$id = bcadd($id, 1, 0);
445			}
446
447			self::checkValueTypes($table, $row);
448
449			$sql = 'INSERT INTO '.$table.' ('.implode(',', array_keys($row)).')'.
450					' VALUES ('.implode(',', array_values($row)).')';
451
452			if (!DBexecute($sql)) {
453				self::exception(self::DBEXECUTE_ERROR, _s('SQL statement execution has failed "%1$s".', $sql));
454			}
455		}
456
457		return $resultIds;
458	}
459
460	/**
461	 * Insert batch data into DB.
462	 *
463	 * @param string $table
464	 * @param array  $values pair of fieldname => fieldvalue
465	 * @param bool   $getids
466	 *
467	 * @return array    an array of ids with the keys preserved
468	 */
469	public static function insertBatch($table, $values, $getids = true) {
470		if (empty($values)) {
471			return true;
472		}
473
474		$resultIds = [];
475
476		$tableSchema = self::getSchema($table);
477		$values = self::addMissingFields($tableSchema, $values);
478
479		if ($getids) {
480			$id = self::reserveIds($table, count($values));
481		}
482
483		$newValues = [];
484		foreach ($values as $key => $row) {
485			if ($getids) {
486				$resultIds[$key] = $id;
487				$row[$tableSchema['key']] = $id;
488				$values[$key][$tableSchema['key']] = $id;
489				$id = bcadd($id, 1, 0);
490			}
491			self::checkValueTypes($table, $row);
492			$newValues[] = $row;
493		}
494
495		$fields = array_keys(reset($newValues));
496
497		$sql = self::getDbBackend()->createInsertQuery($table, $fields, $newValues);
498
499		if (!DBexecute($sql)) {
500			self::exception(self::DBEXECUTE_ERROR, _s('SQL statement execution has failed "%1$s".', $sql));
501		}
502
503		return $resultIds;
504	}
505
506	/**
507	 * Update data in DB.
508	 *
509	 * @param string $table
510	 * @param array $data
511	 * @param array $data[...]['values'] pair of fieldname => fieldvalue for SET clause
512	 * @param array $data[...]['where'] pair of fieldname => fieldvalue for WHERE clause
513	 *
514	 * @return array of ids
515	 */
516	public static function update($table, $data) {
517		if (empty($data)) {
518			return true;
519		}
520
521		$tableSchema = self::getSchema($table);
522
523		$data = zbx_toArray($data);
524		foreach ($data as $row) {
525			// check
526			self::checkValueTypes($table, $row['values']);
527			if (empty($row['values'])) {
528				self::exception(self::DBEXECUTE_ERROR, _s('Cannot perform update statement on table "%1$s" without values.', $table));
529			}
530
531			// set creation
532			$sqlSet = '';
533			foreach ($row['values'] as $field => $value) {
534				$sqlSet .= ' '.$field.'='.$value.',';
535			}
536			$sqlSet = rtrim($sqlSet, ',');
537
538			if (!isset($row['where']) || empty($row['where']) || !is_array($row['where'])) {
539				self::exception(self::DBEXECUTE_ERROR, _s('Cannot perform update statement on table "%1$s" without where condition.', $table));
540			}
541
542			// where condition processing
543			$sqlWhere = [];
544			foreach ($row['where'] as $field => $values) {
545				if (!isset($tableSchema['fields'][$field]) || is_null($values)) {
546					self::exception(self::DBEXECUTE_ERROR, _s('Incorrect field "%1$s" name or value in where statement for table "%2$s".', $field, $table));
547				}
548				$values = zbx_toArray($values);
549				sort($values); // sorting ids to prevent deadlocks when two transactions depend on each other
550
551				$sqlWhere[] = dbConditionString($field, $values);
552			}
553
554			// sql execution
555			$sql = 'UPDATE '.$table.' SET '.$sqlSet.' WHERE '.implode(' AND ', $sqlWhere);
556			if (!DBexecute($sql)) {
557				self::exception(self::DBEXECUTE_ERROR, _s('SQL statement execution has failed "%1$s".', $sql));
558			}
559		}
560
561		return true;
562	}
563
564	/**
565	 * Updates the values by the given PK.
566	 *
567	 * @static
568	 *
569	 * @param string $tableName
570	 * @param string $pk
571	 * @param array $values
572	 *
573	 * @return bool
574	 */
575	public static function updateByPk($tableName, $pk, array $values) {
576		return self::update($tableName, [
577			'where' => [self::getPk($tableName) => $pk],
578			'values' => $values
579		]);
580	}
581
582	/**
583	 * Saves the given records to the database. If the record has the primary key set, it is updated, otherwise - a new
584	 * record is inserted. For new records the newly generated PK is added to the result.
585	 *
586	 * @static
587	 *
588	 * @param $tableName
589	 * @param $data
590	 *
591	 * @return array    the same records, that have been passed with the primary keys set for new records
592	 */
593	public static function save($tableName, array $data) {
594		$pk = self::getPk($tableName);
595
596		$newRecords = [];
597		foreach ($data as $key => $record) {
598			// if the pk is set - update the record
599			if (isset($record[$pk])) {
600				self::updateByPk($tableName, $record[$pk], $record);
601			}
602			// if no pk is set, create the record later
603			else {
604				$newRecords[$key] = $data[$key];
605			}
606		}
607
608		// insert the new records
609		if ($newRecords) {
610			$newIds = self::insert($tableName, $newRecords);
611			foreach ($newIds as $key => $id) {
612				$data[$key][$pk] = $id;
613			}
614		}
615
616		return $data;
617	}
618
619	/**
620	 * Replaces the records given in $oldRecords with the ones in $newRecords.
621	 *
622	 * If a record with the same primary key as a new one already exists in the old records, the record is updated
623	 * only if they are different. For new records the newly generated PK is added to the result. Old records that are
624	 * not present in the new records are deleted.
625	 *
626	 * All of the records must have the primary key defined.
627	 *
628	 * @static
629	 *
630	 * @param $tableName
631	 * @param array $oldRecords
632	 * @param array $newRecords
633	 *
634	 * @return array    the new records, that have been passed with the primary keys set for newly inserted records
635	 */
636	public static function replace($tableName, array $oldRecords, array $newRecords) {
637		$pk = self::getPk($tableName);
638		$oldRecords = zbx_toHash($oldRecords, $pk);
639
640		$modifiedRecords = [];
641		foreach ($newRecords as $key => $record) {
642			// if it's a new or modified record - save it later
643			if (!isset($record[$pk]) || self::recordModified($tableName, $oldRecords[$record[$pk]], $record)) {
644				$modifiedRecords[$key] = $record;
645			}
646
647			// remove the existing records from the collection, the remaining ones will be deleted
648			if(isset($record[$pk])) {
649				unset($oldRecords[$record[$pk]]);
650			}
651		}
652
653		// save modified records
654		if ($modifiedRecords) {
655			$modifiedRecords = self::save($tableName, $modifiedRecords);
656
657			// add the new IDs to the new records
658			foreach ($modifiedRecords as $key => $record) {
659				$newRecords[$key][$pk] = $record[$pk];
660			}
661		}
662
663		// delete remaining records
664		if ($oldRecords) {
665			DB::delete($tableName, [
666				$pk => array_keys($oldRecords)
667			]);
668		}
669
670		return $newRecords;
671	}
672
673	/**
674	 * Replaces the records given in $groupedOldRecords with the ones given in $groupedNewRecords.
675	 *
676	 * This method can be used to replace related objects in one-to-many relations. Both old and new records
677	 * must be grouped by the ID of the record they belong to. The records will be matched by position, instead of
678	 * the primary key as in DB::replace(). That is, the first new record will update the first old one, second new
679	 * record - the second old one, etc. Since the records are matched by position, the new records should not contain
680	 * primary keys.
681	 *
682	 * Example 1:
683	 * $old = array(2 => array( array('gitemid' => 1, 'color' => 'FF0000') ));
684	 * $new = array(2 => array( array('color' => '00FF00') ));
685	 * var_dump(DB::replaceByPosition('items', $old, $new));
686	 * // array(array('gitemid' => 1, 'color' => '00FF00'))
687	 *
688	 * The new record updated the old one.
689	 *
690	 * Example 2:
691	 * $old = array(2 => array( array('gitemid' => 1, 'color' => 'FF0000') ));
692	 * $new = array(
693	 *     2 => array(
694	 *         array('color' => '00FF00'),
695	 *         array('color' => '0000FF')
696	 *     )
697	 * );
698	 * var_dump(DB::replaceByPosition('items', $old, $new));
699	 * // array(array('gitemid' => 1, 'color' => '00FF00'), array('gitemid' => 2, 'color' => '0000FF'))
700	 *
701	 * The first record was updated, the second one - created.
702	 *
703	 * Example 3:
704	 * $old = array(
705	 *     2 => array(
706	 *         array('gitemid' => 1, 'color' => 'FF0000'),
707	 *         array('gitemid' => 2, 'color' => '0000FF')
708	 *     )
709	 * );
710	 * $new = array(2 => array( array('color' => '00FF00') ));
711	 * var_dump(DB::replaceByPosition('items', $old, $new));
712	 * // array(array('gitemid' => 1, 'color' => '00FF00'))
713	 *
714	 * The first record was updated, the second one - deleted.
715	 *
716	 * @param string 	$tableName			table to update
717	 * @param array 	$groupedOldRecords	grouped old records
718	 * @param array 	$groupedNewRecords	grouped new records
719	 *
720	 * @return array	array of new records not grouped (!).
721	 */
722	public static function replaceByPosition($tableName, array $groupedOldRecords, array $groupedNewRecords) {
723		$pk = self::getPk($tableName);
724
725		$allOldRecords = [];
726		$allNewRecords = [];
727		foreach ($groupedNewRecords as $key => $newRecords) {
728			// if records exist for the parent object - replace them, otherwise create new records
729			if (isset($groupedOldRecords[$key])) {
730				$oldRecords = $groupedOldRecords[$key];
731
732				// updated the records by position
733				$newRecords = self::mergeRecords($oldRecords, $newRecords, $pk);
734
735				foreach ($oldRecords as $record) {
736					$allOldRecords[] = $record;
737				}
738			}
739
740			foreach ($newRecords as $record) {
741				$allNewRecords[] = $record;
742			}
743		}
744
745		// replace the old records with the new ones
746		return self::replace($tableName, $allOldRecords, $allNewRecords);
747	}
748
749	/**
750	 * Compares the fields, that are present in both records, and returns true if any of the values differ.
751	 *
752	 * @static
753	 * @param $tableName
754	 * @param array $oldRecord
755	 * @param array $newRecord
756	 *
757	 * @return bool
758	 */
759	public static function recordModified($tableName, array $oldRecord, array $newRecord) {
760		foreach ($oldRecord as $field => $value) {
761			if (self::hasField($tableName, $field)
762					&& isset($newRecord[$field])
763					&& (string) $value !== (string) $newRecord[$field]) {
764				return true;
765			}
766		}
767
768		return false;
769	}
770
771	/**
772	 * Replace each record in $oldRecords with a corresponding record in $newRecords, but keep the old record IDs.
773	 * The records are match by position, that is, the first new record, replaces the first old record and etc.
774	 * If there are less $newRecords than $oldRecords, the remaining old records will be discarded.
775	 *
776	 * @param array 	$oldRecords		array of old records
777	 * @param array 	$newRecords		array of new records
778	 * @param string 	$pk				name of the private key column
779	 *
780	 * @return array	array of new records with the primary keys from the old ones
781	 */
782	protected static function mergeRecords(array $oldRecords, array $newRecords, $pk) {
783		$result = [];
784		foreach ($newRecords as $i => $record) {
785			if (isset($oldRecords[$i])) {
786				$record[$pk] = $oldRecords[$i][$pk];
787			}
788
789			$result[] = $record;
790		}
791
792		return $result;
793	}
794
795	/**
796	 * Delete data from DB.
797	 *
798	 * Example:
799	 * DB::delete('applications', array('applicationid'=>array(1, 8, 6)));
800	 * DELETE FROM applications WHERE applicationid IN (1, 8, 6)
801	 *
802	 * DB::delete('applications', array('applicationid'=>array(1), 'templateid'=array(10)));
803	 * DELETE FROM applications WHERE applicationid IN (1) AND templateid IN (10)
804	 *
805	 * @param string $table
806	 * @param array  $wheres pair of fieldname => fieldvalues
807	 * @param bool   $use_or
808	 *
809	 * @return bool
810	 */
811	public static function delete($table, $wheres, $use_or = false) {
812		if (empty($wheres) || !is_array($wheres)) {
813			self::exception(self::DBEXECUTE_ERROR, _s('Cannot perform delete statement on table "%1$s" without where condition.', $table));
814		}
815		$table_schema = self::getSchema($table);
816
817		$sqlWhere = [];
818		foreach ($wheres as $field => $values) {
819			if (!isset($table_schema['fields'][$field]) || is_null($values)) {
820				self::exception(self::DBEXECUTE_ERROR, _s('Incorrect field "%1$s" name or value in where statement for table "%2$s".', $field, $table));
821			}
822			$values = zbx_toArray($values);
823			sort($values); // sorting ids to prevent deadlocks when two transactions depends from each other
824
825			$sqlWhere[] = dbConditionString($field, $values);
826		}
827
828		$sql = 'DELETE FROM '.$table.' WHERE '.implode(($use_or ? ' OR ' : ' AND '), $sqlWhere);
829		if (!DBexecute($sql)) {
830			self::exception(self::DBEXECUTE_ERROR, _s('SQL statement execution has failed "%1$s"', $sql));
831		}
832
833		return true;
834	}
835
836	/**
837	 * Check if $type is numeric field type.
838	 *
839	 * @param int $type
840	 *
841	 * @return bool
842	 */
843	public static function isNumericFieldType($type) {
844		switch ($type) {
845			case self::FIELD_TYPE_ID:
846			case self::FIELD_TYPE_INT:
847			case self::FIELD_TYPE_UINT:
848				return true;
849		}
850
851		return false;
852	}
853}
854