. **/ class DB { const SCHEMA_FILE = 'schema.inc.php'; const DBEXECUTE_ERROR = 1; const RESERVEIDS_ERROR = 2; const SCHEMA_ERROR = 3; const INIT_ERROR = 4; const FIELD_TYPE_INT = 0x01; const FIELD_TYPE_CHAR = 0x02; const FIELD_TYPE_ID = 0x04; const FIELD_TYPE_FLOAT = 0x08; const FIELD_TYPE_UINT = 0x10; const FIELD_TYPE_BLOB = 0x20; const FIELD_TYPE_TEXT = 0x40; const FIELD_TYPE_CUID = 0x80; const SUPPORTED_FILTER_TYPES = self::FIELD_TYPE_INT | self::FIELD_TYPE_CHAR | self::FIELD_TYPE_ID | self::FIELD_TYPE_FLOAT | self::FIELD_TYPE_UINT | self::FIELD_TYPE_CUID; const SUPPORTED_SEARCH_TYPES = self::FIELD_TYPE_CHAR | self::FIELD_TYPE_TEXT | self::FIELD_TYPE_CUID; /** * Maximum number of IDs per SQL request. * * @var int */ private const CHUNK_SIZE = 1000; private static $schema = null; /** * @var DbBackend */ private static $dbBackend; /** * Get necessary DB class. * * @return DbBackend */ public static function getDbBackend() { global $DB; if (!self::$dbBackend) { switch ($DB['TYPE']) { case ZBX_DB_MYSQL: self::$dbBackend = new MysqlDbBackend(); break; case ZBX_DB_POSTGRESQL: self::$dbBackend = new PostgresqlDbBackend(); break; } } return self::$dbBackend; } private static function exception($code, $error) { throw new DBException($error, $code); } /** * Reserve IDs for primary key of passed table. * If record for table does not exist or value is out of range, ids record is created using maximum ID from table * or minimum allowed value. * * @param string $table table name * @param int $count number of IDs to reserve * * @throws APIException * * @return string */ public static function reserveIds(string $table, int $count): string { $table_schema = self::getSchema($table); $pk_field_name = $table_schema['key']; $sql = 'SELECT nextid'. ' FROM ids'. ' WHERE table_name='.zbx_dbstr($table). ' AND field_name='.zbx_dbstr($pk_field_name). ' FOR UPDATE'; $resource = DBselect($sql); if ($resource === false) { self::exception(self::DBEXECUTE_ERROR, _('Database error occurred.')); } $res = DBfetch($resource); if (!$res) { return self::refreshIds($table, $count); } $max_nextid = bcadd($res['nextid'], $count, 0); if (bccomp($max_nextid, ZBX_DB_MAX_ID) == 1) { return self::refreshIds($table, $count); } $sql = 'UPDATE ids'. ' SET nextid='.$max_nextid. ' WHERE table_name='.zbx_dbstr($table). ' AND field_name='.zbx_dbstr($pk_field_name); if (!DBexecute($sql)) { self::exception(self::DBEXECUTE_ERROR, _('Database error occurred.')); } return bcadd($res['nextid'], 1, 0); } /** * Refresh ID record for given table. * Record is deleted and then created again with value of maximum ID from table or minimum allowed. * * @param string $table table name * @param int $count number of IDs to reserve * * @throws APIException * * @return string */ public static function refreshIds(string $table, int $count): string { $tableSchema = self::getSchema($table); $id_name = $tableSchema['key']; // when we reach the maximum ID, we try to refresh them to check if any IDs have been freed $sql = 'DELETE FROM ids WHERE table_name='.zbx_dbstr($table).' AND field_name='.zbx_dbstr($id_name); if (!DBexecute($sql)) { self::exception(self::DBEXECUTE_ERROR, _('Database error occurred.')); } $row = DBfetch(DBselect('SELECT MAX('.$id_name.') AS id FROM '.$table)); $nextid = ($row && $row['id']) ? $row['id'] : 0; $maxNextId = bcadd($nextid, $count, 0); if (bccomp($maxNextId, ZBX_DB_MAX_ID) == 1) { self::exception( self::RESERVEIDS_ERROR, __METHOD__.' ID greater than maximum allowed for table "'.$table.'"' ); } $sql = 'INSERT INTO ids (table_name,field_name,nextid)'. ' VALUES ('.zbx_dbstr($table).','.zbx_dbstr($id_name).','.$maxNextId.')'; if (!DBexecute($sql)) { self::exception(self::DBEXECUTE_ERROR, _('Database error occurred.')); } $nextid = bcadd($nextid, 1, 0); return $nextid; } /** * Returns an array describing the database schema. * * If the $table parameter is passed, the method will return the schema for the given table, * otherwise - for the whole database. * * @param string|null $table * * @throws DBException if the given table does not exist. * * @return array */ public static function getSchema(?string $table = null): array { if (self::$schema === null) { self::$schema = include __DIR__.'/../../'.self::SCHEMA_FILE; } if ($table === null) { return self::$schema; } if (!array_key_exists($table, self::$schema)) { self::exception(self::SCHEMA_ERROR, _s('Table "%1$s" does not exist.', $table)); } return self::$schema[$table]; } /** * Returns the names of the fields that are used as the primary key of the table. * * @param string $table_name * * @return string */ public static function getPk(string $table_name): string { $schema = self::getSchema($table_name); return $schema['key']; } /** * Returns true if the table $tableName has the $fieldName field. * * @param string $tableName * @param string $fieldName * * @return bool */ public static function hasField($tableName, $fieldName) { $schema = self::getSchema($tableName); return isset($schema['fields'][$fieldName]); } /** * Returns length of the field by schema. * * @param array $field_schema * * @return int */ public static function getFieldLengthBySchema(array $field_schema): int { return $field_schema['length']; } /** * Returns length of the field. * * @param string $table_name * @param string $field_name * * @return int */ public static function getFieldLength(string $table_name, string $field_name): int { return self::getFieldLengthBySchema(self::getSchema($table_name)['fields'][$field_name]); } public static function getDefaults($table) { $table = self::getSchema($table); $defaults = []; foreach ($table['fields'] as $name => $field) { if (isset($field['default'])) { $defaults[$name] = $field['default']; } } return $defaults; } /** * Returns the default value of the given field. * * @param string $table name of the table * @param string $field name of the field * * @return string|null */ public static function getDefault($table, $field) { $table = self::getSchema($table); $field = $table['fields'][$field]; return isset($field['default']) ? $field['default'] : null; } /** * Get the updated values of a record by correctly comparing the new and old ones, taking field types into account. * * @param string $table_name * @param array $new_values * @param array $old_values * * @return array */ public static function getUpdatedValues(string $table_name, array $new_values, array $old_values): array { $updated_values = []; // Discard field names not existing in the target table. $fields = array_intersect_key(self::getSchema($table_name)['fields'], $new_values); foreach ($fields as $name => $spec) { if (!array_key_exists($name, $old_values)) { $updated_values[$name] = $new_values[$name]; continue; } if ($spec['type'] & self::FIELD_TYPE_ID) { if (bccomp($new_values[$name], $old_values[$name]) != 0) { $updated_values[$name] = $new_values[$name]; } } elseif ($spec['type'] & (self::FIELD_TYPE_INT | self::FIELD_TYPE_UINT | self::FIELD_TYPE_FLOAT)) { if ($new_values[$name] != $old_values[$name]) { $updated_values[$name] = $new_values[$name]; } } elseif ($new_values[$name] !== $old_values[$name]) { $updated_values[$name] = $new_values[$name]; } } return $updated_values; } private static function checkValueTypes($tableSchema, &$values) { global $DB; foreach ($values as $field => $value) { if (!isset($tableSchema['fields'][$field])) { unset($values[$field]); continue; } if (isset($tableSchema['fields'][$field]['ref_table'])) { if ($tableSchema['fields'][$field]['null']) { $values[$field] = ($values[$field] == '0') ? NULL : $values[$field]; } } if (is_null($values[$field])) { if ($tableSchema['fields'][$field]['null']) { $values[$field] = 'NULL'; } elseif (isset($tableSchema['fields'][$field]['default'])) { $values[$field] = zbx_dbstr($tableSchema['fields'][$field]['default']); } else { self::exception(self::DBEXECUTE_ERROR, _s('Field "%1$s" cannot be set to NULL.', $field) ); } } else { if ($tableSchema['fields'][$field]['type'] & (self::FIELD_TYPE_CUID | self::FIELD_TYPE_CHAR)) { $length = mb_strlen($values[$field]); if ($length > $tableSchema['fields'][$field]['length']) { 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.', $values[$field], $field, $length, $tableSchema['fields'][$field]['length'])); } $values[$field] = zbx_dbstr($values[$field]); } elseif ($tableSchema['fields'][$field]['type'] & (self::FIELD_TYPE_ID | self::FIELD_TYPE_UINT)) { if (!zbx_ctype_digit($values[$field])) { self::exception(self::DBEXECUTE_ERROR, _s('Incorrect value "%1$s" for unsigned int field "%2$s".', $values[$field], $field)); } $values[$field] = zbx_dbstr($values[$field]); } elseif ($tableSchema['fields'][$field]['type'] & self::FIELD_TYPE_INT) { if (!zbx_is_int($values[$field])) { self::exception(self::DBEXECUTE_ERROR, _s('Incorrect value "%1$s" for int field "%2$s".', $values[$field], $field)); } $values[$field] = zbx_dbstr($values[$field]); } elseif ($tableSchema['fields'][$field]['type'] & self::FIELD_TYPE_FLOAT) { if (!is_numeric($values[$field])) { self::exception(self::DBEXECUTE_ERROR, _s('Incorrect value "%1$s" for float field "%2$s".', $values[$field], $field)); } $values[$field] = zbx_dbstr($values[$field]); } elseif ($tableSchema['fields'][$field]['type'] & self::FIELD_TYPE_TEXT) { $values[$field] = zbx_dbstr($values[$field]); } elseif ($tableSchema['fields'][$field]['type'] & self::FIELD_TYPE_BLOB) { switch ($DB['TYPE']) { case ZBX_DB_MYSQL: $values[$field] = zbx_dbstr($values[$field]); break; case ZBX_DB_POSTGRESQL: $values[$field] = "'".pg_escape_bytea($DB['DB'], $values[$field])."'"; break; } } } } } /** * Returns the records that match the given criteria. * * @param string $tableName * @param array $criteria An associative array of field-value pairs, where value can be either a single value * or an array (IN) * * @return array */ public static function find($tableName, array $criteria = []) { // build the WHERE part $sqlWhere = []; foreach ($criteria as $field => $value) { // check if the table has this field if (!self::hasField($tableName, $field)) { self::exception(self::DBEXECUTE_ERROR, _s('Table "%1$s" doesn\'t have a field named "%2$s".', $tableName, $field)); } $sqlWhere[] = dbConditionString($field, zbx_toArray($value)); } // build query $sql = 'SELECT * FROM '.$tableName; if ($sqlWhere) { $sql .= ' WHERE '.implode(' AND ', $sqlWhere); } return DBfetchArray(DBSelect($sql)); } /** * Insert data into DB. * * @param string $table * @param array $values pair of fieldname => fieldvalue * @param bool $getids * * @return array an array of ids with the keys preserved */ public static function insert($table, $values, $getids = true) { $table_schema = self::getSchema($table); $fields = array_reduce($values, 'array_merge', []); $fields = array_intersect_key($fields, $table_schema['fields']); foreach ($fields as $field => &$value) { $value = array_key_exists('default', $table_schema['fields'][$field]) ? $table_schema['fields'][$field]['default'] : null; } unset($value); foreach ($values as $key => &$row) { $row = array_merge($fields, $row); } unset($row); return self::insertBatch($table, $values, $getids); } /** * Returns the list of mandatory fields with default values for INSERT statements. * * @param array $table_schema * * @return array */ private static function getMandatoryFields(array $table_schema): array { global $DB; $mandatory_fields = []; if ($DB['TYPE'] === ZBX_DB_MYSQL) { foreach ($table_schema['fields'] as $name => $field) { if ($field['type'] & self::FIELD_TYPE_TEXT) { $mandatory_fields += [$name => $field['default']]; } } } return $mandatory_fields; } /** * Add IDs to inserted rows. * * @param string $table * @param array $values * * @return array An array of IDs with the keys preserved. */ private static function addIds(string $table, array &$values): array { $table_schema = self::getSchema($table); $resultids = []; if ($table_schema['fields'][$table_schema['key']]['type'] & self::FIELD_TYPE_ID) { $id = self::reserveIds($table, count($values)); } foreach ($values as $key => &$row) { if ($table_schema['fields'][$table_schema['key']]['type'] & self::FIELD_TYPE_ID) { $resultids[$key] = $id; $row = [$table_schema['key'] => $id] + $row; $id = bcadd($id, 1, 0); } elseif ($table_schema['fields'][$table_schema['key']]['type'] & self::FIELD_TYPE_CUID) { $id = CCuid::generate(); $resultids[$key] = $id; $row = [$table_schema['key'] => $id] + $row; } } unset($row); return $resultids; } /** * Insert batch data into DB. * * @param string $table * @param array $values pair of fieldname => fieldvalue * @param bool $getids * * @return array An array of IDs with the keys preserved. */ public static function insertBatch($table, $values, $getids = true) { if (empty($values)) { return true; } $resultids = []; $table_schema = self::getSchema($table); $mandatory_fields = self::getMandatoryFields($table_schema); if ($getids) { $resultids = self::addIds($table, $values); } foreach ($values as &$row) { $row += $mandatory_fields; self::checkValueTypes($table_schema, $row); self::uppercaseValues($table, $row); } unset($row); $sql = self::getDbBackend()->createInsertQuery($table, array_keys(reset($values)), $values); if (!DBexecute($sql)) { self::exception(self::DBEXECUTE_ERROR, _s('SQL statement execution has failed "%1$s".', $sql)); } return $resultids; } /** * Update data in DB. * * @param string $table_name * @param array $upd_requests * @param array $upd_requests[]['values'] Pair of fieldname => fieldvalue for SET clause. * @param array $upd_requests[]['where'] Pair of fieldname => fieldvalue for WHERE clause. */ public static function update(string $table_name, array $upd_requests): void { if (!$upd_requests) { return; } $table_schema = self::getSchema($table_name); $upd_requests = zbx_toArray($upd_requests); foreach ($upd_requests as $upd_request) { self::validateUpdValues($table_name, $table_schema, $upd_request); self::validateUpdConditions($table_name, $table_schema, $upd_request); if (self::hasCompositePk($table_schema)) { self::updateByConditions($table_name, $upd_request); } elseif (count($upd_request['where']) == 1 && array_key_exists($table_schema['key'], $upd_request['where'])) { self::updateByPkIds($table_name, $upd_request); } else { self::updateExistingRowsByPkIds($table_name, $upd_request, $table_schema['key']); } } } private static function validateUpdValues(string $table_name, array $table_schema, array &$upd_request): void { if (!array_key_exists('values', $upd_request) || !$upd_request['values']) { self::exception(self::DBEXECUTE_ERROR, _s('Cannot perform update statement on table "%1$s" without values.', $table_name) ); } self::checkValueTypes($table_schema, $upd_request['values']); self::uppercaseValues($table_name, $upd_request['values']); } private static function validateUpdConditions(string $table_name, array $table_schema, array &$upd_request): void { if (!array_key_exists('where', $upd_request) || !$upd_request['where']) { self::exception(self::DBEXECUTE_ERROR, _s('Cannot perform update statement on table "%1$s" without where condition.', $table_name) ); } foreach ($upd_request['where'] as $field => &$values) { if (!array_key_exists($field, $table_schema['fields']) || $values === null) { self::exception(self::DBEXECUTE_ERROR, _s('Incorrect field "%1$s" name or value in where statement for table "%2$s".', $field, $table_name) ); } $values = zbx_toArray($values); sort($values); // Sorting IDs to prevent deadlocks when two transactions depend on each other. } unset($values); } private static function updateByConditions(string $table_name, array $upd_request): void { $sql_where = []; foreach ($upd_request['where'] as $field => $values) { $sql_where[] = dbConditionString($field, $values); } $sql = 'UPDATE '.$table_name. ' SET '.self::getUpdSqlValues($upd_request). ' WHERE '.implode(' AND ', $sql_where); if (!DBexecute($sql)) { self::exception(self::DBEXECUTE_ERROR, _s('SQL statement execution has failed "%1$s".', $sql)); } } private static function updateByPkIds(string $table_name, array $upd_request): void { $sql_values = self::getUpdSqlValues($upd_request); $pk_field_name = key($upd_request['where']); $chunkids = []; foreach ($upd_request['where'][$pk_field_name] as $id) { $chunkids[] = $id; if (count($chunkids) == self::CHUNK_SIZE) { self::updateByIdField($table_name, $sql_values, $pk_field_name, $chunkids); $chunkids = []; } } if ($chunkids) { self::updateByIdField($table_name, $sql_values, $pk_field_name, $chunkids); } } private static function updateExistingRowsByPkIds(string $table_name, array $upd_request, string $pk_field_name): void { $options = [ 'output' => [$pk_field_name], 'filter' => $upd_request['where'], 'sortfield' => [$pk_field_name] ]; $resource = DBselect(self::makeSql($table_name, $options)); $sql_values = self::getUpdSqlValues($upd_request); $chunkids = []; while ($pk_row = DBfetch($resource)) { $chunkids[] = $pk_row[$pk_field_name]; if (count($chunkids) == self::CHUNK_SIZE) { self::updateByIdField($table_name, $sql_values, $pk_field_name, $chunkids); $chunkids = []; } } if ($chunkids) { self::updateByIdField($table_name, $sql_values, $pk_field_name, $chunkids); } } private static function getUpdSqlValues(array $upd_request): string { $sql_values = ''; foreach ($upd_request['values'] as $field_name => $value) { if ($sql_values !== '') { $sql_values .= ','; } $sql_values .= $field_name.'='.$value; } return $sql_values; } private static function updateByIdField(string $table_name, string $sql_values, string $id_field_name, array $ids): void { $sql = 'UPDATE '.$table_name. ' SET '.$sql_values. ' WHERE '.dbConditionId($id_field_name, $ids); if (!DBexecute($sql)) { self::exception(self::DBEXECUTE_ERROR, _s('SQL statement execution has failed "%1$s".', $sql)); } } /** * Updates the values by the given PK. * * @param string $tableName * @param string $pk * @param array $values * * @return bool */ public static function updateByPk($tableName, $pk, array $values) { return self::update($tableName, [ 'where' => [self::getPk($tableName) => $pk], 'values' => $values ]); } /** * Saves the given records to the database. If the record has the primary key set, it is updated, otherwise - a new * record is inserted. For new records the newly generated PK is added to the result. * * @param $tableName * @param $data * * @return array the same records, that have been passed with the primary keys set for new records */ public static function save($tableName, array $data) { $pk = self::getPk($tableName); $newRecords = []; foreach ($data as $key => $record) { // if the pk is set - update the record if (isset($record[$pk])) { self::updateByPk($tableName, $record[$pk], $record); } // if no pk is set, create the record later else { $newRecords[$key] = $data[$key]; } } // insert the new records if ($newRecords) { $newIds = self::insert($tableName, $newRecords); foreach ($newIds as $key => $id) { $data[$key][$pk] = $id; } } return $data; } /** * Replaces the records given in $oldRecords with the ones in $newRecords. * * If a record with the same primary key as a new one already exists in the old records, the record is updated * only if they are different. For new records the newly generated PK is added to the result. Old records that are * not present in the new records are deleted. * * All of the records must have the primary key defined. * * @param string $tableName * @param array $oldRecords * @param array $newRecords * * @return array the new records, that have been passed with the primary keys set for newly inserted records */ public static function replace($tableName, array $oldRecords, array $newRecords) { $pk = self::getPk($tableName); $oldRecords = zbx_toHash($oldRecords, $pk); $modifiedRecords = []; foreach ($newRecords as $key => $record) { // if it's a new or modified record - save it later if (!isset($record[$pk]) || self::recordModified($tableName, $oldRecords[$record[$pk]], $record)) { $modifiedRecords[$key] = $record; } // remove the existing records from the collection, the remaining ones will be deleted if(isset($record[$pk])) { unset($oldRecords[$record[$pk]]); } } // save modified records if ($modifiedRecords) { $modifiedRecords = self::save($tableName, $modifiedRecords); // add the new IDs to the new records foreach ($modifiedRecords as $key => $record) { $newRecords[$key][$pk] = $record[$pk]; } } // delete remaining records if ($oldRecords) { self::delete($tableName, [ $pk => array_keys($oldRecords) ]); } return $newRecords; } /** * Compares the fields, that are present in both records, and returns true if any of the values differ. * * @param string $tableName * @param array $oldRecord * @param array $newRecord * * @return bool */ public static function recordModified($tableName, array $oldRecord, array $newRecord) { foreach ($oldRecord as $field => $value) { if (self::hasField($tableName, $field) && isset($newRecord[$field]) && (string) $value !== (string) $newRecord[$field]) { return true; } } return false; } /** * Delete data from DB. * * Example: * DB::delete('items', ['itemid' => [1, 8, 6]]); * DELETE FROM items WHERE itemid IN (1,8,6) * * DB::delete('items', ['itemid' => [1], 'templateid' => [10, 21]]); * DELETE FROM items WHERE itemid=1 AND templateid IN (10,21) * * @param string $table_name * @param array $del_conditions */ public static function delete(string $table_name, array $del_conditions): void { $table_schema = self::getSchema($table_name); self::validateDelConditions($table_name, $table_schema, $del_conditions); if (self::hasCompositePk($table_schema)) { self::deleteByConditions($table_name, $del_conditions); } elseif (array_key_exists($table_schema['key'], $del_conditions) && count($del_conditions) == 1) { self::deleteByPkIds($table_name, $del_conditions); } else { self::deleteExistingRowsByPkIds($table_name, $del_conditions, $table_schema['key']); } } private static function validateDelConditions(string $table_name, array $table_schema, array &$del_conditions): void { if (!$del_conditions) { self::exception(self::DBEXECUTE_ERROR, _s('Cannot perform delete statement on table "%1$s" without where condition.', $table_name) ); } foreach ($del_conditions as $field => &$values) { if (!array_key_exists($field, $table_schema['fields']) || $values === null) { self::exception(self::DBEXECUTE_ERROR, _s('Incorrect field "%1$s" name or value in where statement for table "%2$s".', $field, $table_name) ); } $values = zbx_toArray($values); sort($values); // Sorting IDs to prevent deadlocks when two transactions depend on each other. } unset($values); } private static function hasCompositePk(array $table_schema): bool { return strpos($table_schema['key'], ',') !== false; } private static function deleteByConditions(string $table_name, array $del_conditions): void { $sql_where = []; foreach ($del_conditions as $field => $values) { $sql_where[] = dbConditionString($field, $values); } $sql = 'DELETE FROM '.$table_name. ' WHERE '.implode(' AND ', $sql_where); if (!DBexecute($sql)) { self::exception(self::DBEXECUTE_ERROR, _s('SQL statement execution has failed "%1$s"', $sql)); } } private static function deleteByPkIds(string $table_name, array $del_conditions): void { $pk_field_name = key($del_conditions); $chunkids = []; foreach ($del_conditions[$pk_field_name] as $id) { $chunkids[] = $id; if (count($chunkids) == self::CHUNK_SIZE) { self::deleteByIdField($table_name, $pk_field_name, $chunkids); $chunkids = []; } } if ($chunkids) { self::deleteByIdField($table_name, $pk_field_name, $chunkids); } } private static function deleteExistingRowsByPkIds(string $table_name, array $del_conditions, string $pk_field_name): void { $options = [ 'output' => [$pk_field_name], 'filter' => $del_conditions, 'sortfield' => [$pk_field_name] ]; $resource = DBselect(self::makeSql($table_name, $options)); $chunkids = []; while ($row = DBfetch($resource)) { $chunkids[] = $row[$pk_field_name]; if (count($chunkids) == self::CHUNK_SIZE) { self::deleteByIdField($table_name, $pk_field_name, $chunkids); $chunkids = []; } } if ($chunkids) { self::deleteByIdField($table_name, $pk_field_name, $chunkids); } } private static function deleteByIdField(string $table_name, string $id_field_name, array $ids): void { $sql = 'DELETE FROM '.$table_name. ' WHERE '.dbConditionId($id_field_name, $ids); if (!DBexecute($sql)) { self::exception(self::DBEXECUTE_ERROR, _s('SQL statement execution has failed "%1$s"', $sql)); } } /** * @param string $table_name * @param array $options * @param string $table_alias * * @return string */ public static function makeSql($table_name, array &$options, $table_alias = null) { $defaults = [ 'output' => [], 'countOutput' => false, 'filter' => [], 'search' => [], 'startSearch' => false, 'searchByAny' => false, 'sortfield' => [], 'sortorder' => [], 'limit' => null, 'preservekeys' => false ]; if ($array_diff = array_diff_key($options, $defaults)) { unset($array_diff[self::getPk($table_name).'s']); if ($array_diff) { self::exception(self::SCHEMA_ERROR, vsprintf('%s: unsupported option "%s".', [__FUNCTION__, key($array_diff)]) ); } } $options = zbx_array_merge($defaults, $options); $sql_parts = self::createSelectQueryParts($table_name, $options, $table_alias); return 'SELECT '.implode(',', $sql_parts['select']). ' FROM '.implode(',', $sql_parts['from']). ($sql_parts['where'] ? ' WHERE '.implode(' AND ', $sql_parts['where']) : ''). ($sql_parts['order'] ? ' ORDER BY '.implode(',', $sql_parts['order']) : ''); } /** * @param string $table_name * @param array $options * @param string $table_alias * * @return array */ public static function select($table_name, array $options, $table_alias = null) { $db_result = DBSelect(self::makeSql($table_name, $options, $table_alias), $options['limit']); if ($options['countOutput']) { return DBfetch($db_result)['rowscount']; } $result = []; $field_names = array_flip($options['output']); if ($options['preservekeys']) { $pk = self::getPk($table_name); while ($db_row = DBfetch($db_result)) { $result[$db_row[$pk]] = array_intersect_key($db_row, $field_names); } } else { while ($db_row = DBfetch($db_result)) { $result[] = array_intersect_key($db_row, $field_names); } } return $result; } /** * Returns the table name with the table alias. * * @param string $table_name * @param string $table_alias * * @return string */ private static function tableId($table_name, $table_alias = null) { return($table_alias !== null) ? $table_name.' '.$table_alias : $table_name; } /** * Prepends the table alias to the given field name. * * @param string $field_name * @param string $table_alias * * @return string */ private static function fieldId($field_name, $table_alias = null) { return ($table_alias !== null) ? $table_alias.'.'.$field_name : $field_name; } /** * Convert field to uppercase or substitute it with its pre-upcased variant. * * @param string $field_name * @param string $table_name * @param string|null $table_alias * * @return string */ public static function uppercaseField(string $field_name, string $table_name, ?string $table_alias = null): string { if ($table_alias === null) { $table_alias = $table_name; } if ($field_name === 'name' && self::hasField($table_name, 'name_upper')) { return $table_alias.'.name_upper'; } if ($field_name === 'name_resolved' && self::hasField($table_name, 'name_resolved_upper')) { return $table_alias.'.name_resolved_upper'; } return 'UPPER('.$table_alias.'.'.$field_name.')'; } /** * Convert field values to uppercase. * * @param string $table_name * @param array $values */ public static function uppercaseValues(string $table_name, array &$values): void { if (array_key_exists('name_resolved', $values) && self::hasField($table_name, 'name_resolved_upper')) { $values['name_resolved_upper'] = 'UPPER('.$values['name_resolved'].')'; } } /** * Builds an SQL parts array from the given options. * * @param string $table_name * @param array $options * @param string $table_alias * * @return array The resulting SQL parts array */ private static function createSelectQueryParts($table_name, array $options, $table_alias = null) { $sql_parts = [ 'select' => [], 'from' => [self::tableId($table_name, $table_alias)], 'where' => [], 'order' => [] ]; // add output options $sql_parts = self::applyQueryOutputOptions($table_name, $options, $table_alias, $sql_parts); // add filter options $sql_parts = self::applyQueryFilterOptions($table_name, $options, $table_alias, $sql_parts); // add search options $sql_parts = self::applyQuerySearchOptions($table_name, $options, $table_alias, $sql_parts); // add sort options $sql_parts = self::applyQuerySortOptions($table_name, $options, $table_alias, $sql_parts); return $sql_parts; } /** * Modifies the SQL parts to implement all of the output related options. * * @param string $table_name * @param array $options * @param string|null $table_alias * @param array $sql_parts * * @throws APIException * @throws DBException * @return array */ private static function applyQueryOutputOptions($table_name, array $options, $table_alias, array $sql_parts) { if ($options['countOutput']) { $sql_parts['select'][] = 'COUNT('.self::fieldId('*', $table_alias).') AS rowscount'; } else { $table_schema = self::getSchema($table_name); $select = []; $select[self::fieldId(self::getPk($table_name), $table_alias)] = true; foreach ($options['output'] as $field_name) { if (!array_key_exists($field_name, $table_schema['fields'])) { self::exception(self::SCHEMA_ERROR, vsprintf('%s: field "%s.%s" does not exist.', [__FUNCTION__, $table_name, $field_name]) ); } $select[self::fieldId($field_name, $table_alias)] = true; } $sql_parts['select'] = array_keys($select); } return $sql_parts; } /** * Modifies the SQL parts to implement all the filter related options. * * @param string $table_name * @param array $options * @param string|null $table_alias * @param array $sql_parts * * @throws APIException * @return array */ private static function applyQueryFilterOptions($table_name, array $options, $table_alias, array $sql_parts) { $table_schema = self::getSchema($table_name); $pk = self::getPk($table_name); $pk_option = $pk.'s'; // pks if (array_key_exists($pk_option, $options)) { $options[$pk_option] = (array) $options[$pk_option]; $field_schema = $table_schema['fields'][$pk]; $field_name = self::fieldId($pk, $table_alias); if ($field_schema['type'] & self::FIELD_TYPE_ID) { $sql_parts['where'][] = dbConditionId($field_name, $options[$pk_option]); } elseif ($field_schema['type'] & (self::FIELD_TYPE_INT | self::FIELD_TYPE_UINT)) { $sql_parts['where'][] = dbConditionInt($field_name, $options[$pk_option]); } else { $sql_parts['where'][] = dbConditionString($field_name, $options[$pk_option]); } } // filters if (is_array($options['filter'])) { $sql_parts = self::dbFilter($table_name, $options, $table_alias, $sql_parts); } return $sql_parts; } /** * Modifies the SQL parts to implement all of the search related options. * * @param string $table_name * @param array $options * @param array $options['search'] * @param bool $options['startSearch'] * @param bool $options['searchByAny'] * @param string|null $table_alias * @param array $sql_parts * * @throws APIException * @throws DBException * @return array */ private static function applyQuerySearchOptions($table_name, array $options, $table_alias, array $sql_parts) { global $DB; $table_schema = self::getSchema($table_name); $start = $options['startSearch'] ? '' : '%'; $glue = $options['searchByAny'] ? ' OR ' : ' AND '; $search = []; foreach ($options['search'] as $field_name => $patterns) { if (!array_key_exists($field_name, $table_schema['fields'])) { self::exception(self::SCHEMA_ERROR, vsprintf('%s: field "%s.%s" does not exist.', [__FUNCTION__, $table_name, $field_name]) ); } $field_schema = $table_schema['fields'][$field_name]; if (($field_schema['type'] & self::SUPPORTED_SEARCH_TYPES) == 0) { self::exception(self::SCHEMA_ERROR, vsprintf('%s: field "%s.%s" has an unsupported type.', [__FUNCTION__, $table_name, $field_name]) ); } if ($patterns === null) { continue; } foreach ((array) $patterns as $pattern) { // escaping parameter that is about to be used in LIKE statement $pattern = mb_strtoupper(strtr($pattern, ['!' => '!!', '%' => '!%', '_' => '!_'])); $pattern = zbx_dbstr($start.$pattern.'%'); $search[] = self::uppercaseField($field_name, $table_name, $table_alias).' LIKE '.$pattern." ESCAPE '!'"; } } if ($search) { $sql_parts['where'][] = ($options['searchByAny'] && count($search) > 1) ? '('.implode($glue, $search).')' : implode($glue, $search); } return $sql_parts; } /** * Apply filter conditions to sql built query. * * @param string $table_name * @param array $options * @param string|null $table_alias * @param array $sql_parts * * @throws APIException * @throws DBException * @return array */ private static function dbFilter($table_name, $options, $table_alias, $sql_parts) { $table_schema = self::getSchema($table_name); $filter = []; foreach ($options['filter'] as $field_name => $value) { if (!array_key_exists($field_name, $table_schema['fields'])) { self::exception(self::SCHEMA_ERROR, vsprintf('%s: field "%s.%s" does not exist.', [__FUNCTION__, $table_name, $field_name]) ); } $field_schema = $table_schema['fields'][$field_name]; if (($field_schema['type'] & self::SUPPORTED_FILTER_TYPES) == 0) { self::exception(self::SCHEMA_ERROR, vsprintf('%s: field "%s.%s" has an unsupported type.', [__FUNCTION__, $table_name, $field_name]) ); } if ($value === null) { continue; } if (!is_array($value)) { $value = [$value]; } if ($field_schema['type'] & self::FIELD_TYPE_ID) { $filter[] = dbConditionId(self::fieldId($field_name, $table_alias), $value); } elseif ($field_schema['type'] & (self::FIELD_TYPE_INT | self::FIELD_TYPE_UINT)) { $filter[] = dbConditionInt(self::fieldId($field_name, $table_alias), $value); } else { $filter[] = dbConditionString(self::fieldId($field_name, $table_alias), $value); } } if ($filter) { $sql_parts['where'][] = implode(' AND ', $filter); } return $sql_parts; } /** * Get array of the field names by which filtering is supported from the given table. * If $output_fields parameter is given, get filterable fields among them in scope of the given table name. * * @param string $table_name * @param array|null $output_fields * * @return array */ public static function getFilterFields(string $table_name, ?array $output_fields = null): array { $table_schema = self::getSchema($table_name); if ($output_fields !== null) { $table_schema['fields'] = array_intersect_key($table_schema['fields'], array_flip($output_fields)); } $filter_fields = []; foreach ($table_schema['fields'] as $field_name => $field_schema) { if ($field_schema['type'] & self::SUPPORTED_FILTER_TYPES) { $filter_fields[] = $field_name; } } return $filter_fields; } /** * Get array of the field names by which searching is supported from the given table. * If $output_fields parameter is given, get searchable fields among them in scope of the given table name. * * @param string $table_name * @param array|null $output_fields * * @return array */ public static function getSearchFields(string $table_name, ?array $output_fields = null): array { $table_schema = self::getSchema($table_name); if ($output_fields !== null) { $table_schema['fields'] = array_intersect_key($table_schema['fields'], array_flip($output_fields)); } $search_fields = []; foreach ($table_schema['fields'] as $field_name => $field_schema) { if ($field_schema['type'] & self::SUPPORTED_SEARCH_TYPES) { $search_fields[] = $field_name; } } return $search_fields; } /** * Modifies the SQL parts to implement all of the sorting related options. * * @param string $table_name * @param array $options * @param string|null $table_alias * @param array $sql_parts * * @throws APIException * @throws DBException * @return array */ private static function applyQuerySortOptions($table_name, array $options, $table_alias, array $sql_parts) { $table_schema = self::getSchema($table_name); foreach ($options['sortfield'] as $index => $field_name) { if (!array_key_exists($field_name, $table_schema['fields'])) { self::exception(self::SCHEMA_ERROR, vsprintf('%s: field "%s.%s" does not exist.', [__FUNCTION__, $table_name, $field_name]) ); } $sortorder = ''; if (array_key_exists($index, $options['sortorder']) && $options['sortorder'][$index] == ZBX_SORT_DOWN) { $sortorder = ' '.ZBX_SORT_DOWN; } $sql_parts['order'][] = self::fieldId($field_name, $table_alias).$sortorder; } return $sql_parts; } }