. * * @link https://www.librenms.org * * @copyright 2019 Tony Murray * @author Tony Murray */ namespace LibreNMS\Alerting; use DB; use Illuminate\Database\Query\Builder; use Illuminate\Support\Str; use Log; class QueryBuilderFluentParser extends QueryBuilderParser { /** * Convert the query builder rules to a Laravel Fluent builder * * @return Builder|null */ public function toQuery() { if (empty($this->builder) || ! array_key_exists('condition', $this->builder)) { return null; } $query = DB::table('devices'); $this->joinTables($query); $this->parseGroupToQuery($query, $this->builder); return $query; } /** * @param Builder $query * @param array $rule * @param string $parent_condition AND or OR (for root, this should be null) * @return Builder */ protected function parseGroupToQuery($query, $rule, $parent_condition = null) { return $query->where(function ($query) use ($rule) { foreach ($rule['rules'] as $group_rule) { if (array_key_exists('condition', $group_rule)) { $this->parseGroupToQuery($query, $group_rule, $rule['condition']); } else { $this->parseRuleToQuery($query, $group_rule, $rule['condition']); } } }, null, null, $parent_condition ?? $rule['condition']); } /** * @param Builder $query * @param array $rule * @param string $condition AND or OR * @return Builder */ protected function parseRuleToQuery($query, $rule, $condition) { [$field, $op, $value] = $this->expandRule($rule); switch ($op) { case 'equal': case 'not_equal': case 'less': case 'less_or_equal': case 'greater': case 'greater_or_equal': case 'regex': case 'not_regex': return $query->where($field, self::$operators[$op], $value, $condition); case 'contains': case 'not_contains': return $query->where($field, self::$operators[$op], "%$value%", $condition); case 'begins_with': case 'not_begins_with': return $query->where($field, self::$operators[$op], "$value%", $condition); case 'ends_with': case 'not_ends_with': return $query->where($field, self::$operators[$op], "%$value", $condition); case 'is_empty': case 'is_not_empty': return $query->where($field, self::$operators[$op], ''); case 'is_null': case 'is_not_null': return $query->whereNull($field, $condition, $op == 'is_not_null'); case 'between': case 'not_between': return $query->whereBetween($field, $value, $condition, $op == 'not_between'); case 'in': case 'not_in': $values = preg_split('/[, ]/', $value); if ($values !== false) { return $query->whereIn($field, $values, $condition, $op == 'not_in'); } Log::error('Could not parse in values, use comma or space delimiters'); break; default: Log::error('Unhandled QueryBuilderFluentParser operation: ' . $op); } return $query; } /** * Extract field, operator and value from the rule and expand macros and raw values * * @param array $rule * @return array [field, operator, value] */ protected function expandRule($rule) { $field = $rule['field']; if (Str::startsWith($field, 'macros.')) { $field = DB::raw($this->expandMacro($field)); } $op = $rule['operator']; $value = $rule['value']; if (! is_array($value) && Str::startsWith($value, '`') && Str::endsWith($value, '`')) { $value = DB::raw($this->expandMacro(trim($value, '`'))); } return [$field, $op, $value]; } /** * @param Builder $query * @return Builder */ protected function joinTables($query) { if (! isset($this->builder['joins'])) { $this->generateJoins(); } foreach ($this->builder['joins'] as $join) { [$rightTable, $left, $right] = $join; $query->leftJoin($rightTable, $left, $right); } return $query; } /** * Generate the joins for this rule and store them in the rule. * This is an expensive operation. * * @return $this */ public function generateJoins() { $joins = []; foreach ($this->generateGlue() as $glue) { [$left, $right] = explode(' = ', $glue, 2); if (Str::contains($right, '.')) { // last line is devices.device_id = ? for alerting... ignore it [$leftTable, $leftKey] = explode('.', $left); [$rightTable, $rightKey] = explode('.', $right); $target_table = ($rightTable != 'devices' ? $rightTable : $leftTable); // don't try to join devices $joins[] = [$target_table, $left, $right]; } } $this->builder['joins'] = $joins; return $this; } }