Module: Sixty::Shape
- Defined in:
- lib/sixty/shape.rb
Overview
Argument shape: what a query is, with none of what it is about.
SECURITY BOUNDARY, and the document-database counterpart to Sixty::Sql.
For SQL, identity is the statement with its literals stripped — a string with things taken out of it. That approach has no analogue here. A Mongo filter is a tree in which the values sit beside the keys, arbitrarily deep, so there is no text to lex and nothing to remove: the values are structurally interleaved with the only part we are allowed to keep.
So this walk never removes anything. It emits keys, and a value has no path to the output at all — not a redaction that could miss a case, but a construction in which the unsafe half is unreachable. If you are tempted to add "just the value, when it is a small integer, for readability": don't. That is the change that turns a structural guarantee back into a filter with exceptions.
The other job is bounding cardinality, exactly as SQL normalization does. Identity that moved with the data would make every query a new operation, and an operation with no history has nothing to drift against.
Constant Summary collapse
- MAX_DEPTH =
Bounds on the walk, so a pathological argument cannot produce an unbounded identity. Both sit far above anything a real query reaches, and the depth cap is also what makes a cyclic structure terminate.
6- MAX_KEYS =
24- MAX_STAGES =
How many stages of an ordered structure — an aggregation pipeline — count.
12
Class Method Summary collapse
-
.shape_of(value, depth = 0) ⇒ Object
The shape of one argument tree:
filter{createdAt{$gte},status}. -
.shape_of_sequence(values) ⇒ Object
The shape of an ordered structure, where every element matters.
Class Method Details
.shape_of(value, depth = 0) ⇒ Object
The shape of one argument tree: filter{createdAt{$gte},status}.
Keys are sorted, because two call sites writing the same query with the keys in a different order are the same query, and insertion order would make them two operations that never accumulate a shared history.
Only a Hash or an Array is structure. Everything else is a value,
however many attributes it happens to carry — and that is not a nicety:
a BSON::ObjectId, a Time, a BSON::Regexp or any application object has
internals that are not the query's structure, and walking them would put
meaningless keys into the identity of every query that filters by _id,
which is most of them.
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/sixty/shape.rb', line 48 def shape_of(value, depth = 0) return '' if depth > MAX_DEPTH case value when Array # An array contributes the shape of its first element, never its length. # `{_id: {'$in' => [...]}}` with three ids and with three thousand is one # operation — the same bargain SQL normalization strikes when it folds # `in (?, ?, ?)` down to `in (?)`. value.empty? ? '' : shape_of(value.first, depth + 1) when Hash keys = value.keys.sort_by(&:to_s).first(MAX_KEYS) return '' if keys.empty? keys.map do |key| inner = shape_of(value[key], depth + 1) inner.empty? ? key.to_s : "#{key}{#{inner}}" end.join(',') else '' end end |
.shape_of_sequence(values) ⇒ Object
The shape of an ordered structure, where every element matters.
An aggregation pipeline is a list whose entries are deliberately different
from one another — [{$match}, {$group}, {$sort}] — so it is structure,
not data, and taking only the first element the way shape_of does would
collapse every pipeline in the application to the shape of whatever it
happened to start with. $match → $group and $match → $lookup → $unwind
would be one operation, which is the difference between a working query
and the N+1 that replaced it.
80 81 82 83 84 85 86 87 88 89 |
# File 'lib/sixty/shape.rb', line 80 def shape_of_sequence(values) return '' unless values.is_a?(Array) && !values.empty? stages = values.first(MAX_STAGES).map do |stage| inner = shape_of(stage, 1) inner.empty? ? '[]' : "[#{inner}]" end stages << '[…]' if values.length > MAX_STAGES stages.join end |