I once built a small “predicate builder” on top of ARel, PostgreSQL JSONB support and jQuery QueryBuilder. It wasn’t all powerful because it was servicing a specific need but I was amazed at the power of ARel combined with JSONB operators.
TLDR;
I’m interested in helping, even by just writing documentation on ARel
The engine operated like this:
- the frontend query builder had predicates (or group of predicates) all combined in
AND
s and OR
s
- Ruby received the JSON blob, parsed it and recursively build ARel nodes to combine the queries with proper escaping and all
- custom ARel nodes were used to build JSONB operators that weren’t supported natively
- queries were then unleashed on the DB
The gist was done by stuff like:
module StringOperators
def string_in(model, rule)
# ...
left_op = model[field[:name]]
if field[:type] == :jsonb
# ["#{name} #> '{#{path}}' <@ json_build_array(?)::jsonb", values]
left_op = json_dash_single_arrow(left_op, field[:json_path])
values = field_value.map { |value| Arel::Nodes.build_quoted(value) }
right_op = Arel::Nodes::NamedFunction.new(
'CAST', [Arel::Nodes::NamedFunction.new(
'json_build_array', values).as('jsonb')])
Arel::Nodes::InfixOperation.new('<@', left_op, right_op)
else
left_op.in(field_value)
end
end
end
it also supported arrays:
left_op = json_dash_single_arrow(left_op, field[:json_path])
right_op = Arel::Nodes::SqlLiteral
.new("'{#{field[:value].join(',')}}'")
Arel::Nodes::InfixOperation.new('?|', left_op, right_op)
and datetimes:
value = Time.zone.parse(field[:value]).utc
if field[:type] == :jsonb
left_op = json_dash_double_arrow(left_op, field[:json_path])
value = value.iso8601
end
left_op.public_send(arel_operator, value)
json_dash_single_arrow
and json_dash_double_arrow
were very simple custom functions:
def json_dash_single_arrow(node, path)
Arel::Nodes::InfixOperation.new(
'#>', node, Arel::Nodes.build_quoted("{#{path}}"))
end
def json_dash_double_arrow(node, path)
Arel::Nodes::InfixOperation.new(
'#>>', node, Arel::Nodes.build_quoted("{#{path}}"))
end
all of this stuff was then combined with recursion and .reduce()
to a final SQL query which combined the various operators.
Albeit complicated, it was working beautifully
Models were “ARelized” by using something like:
def [](name)
arel_table[name]
end
I had to “step debug” a lot to figure out how all of this came together as there was no official documentation.