inillucent_sql/bind/json_subtype.rs
1//! The JSON subtype: which values carry SQLite's `J` mark, and how the mark
2//! reaches the JSON call that reads it.
3//!
4//! Invariant: **a subtype is a property of the call that produced a value, and
5//! the binder decides it from the bound tree.** `Value` has no slot for one, so
6//! `subtype()` is answered here from the producing call, and a JSON function's
7//! argument that carries the mark in SQLite but is not itself a JSON call is
8//! wrapped in one. Both rules were measured against the pinned 3.53.4 shell.
9
10use inillucent_value::Collation;
11
12use super::{Binder, BoundExpr, SubqueryKind};
13use crate::ast::ExprId;
14use crate::diagnostic::ParseError;
15use crate::function;
16
17impl Binder<'_> {
18 /// Binds `subtype(X)`.
19 ///
20 /// **`subtype` is answered where the producing function is known.**
21 /// A subtype is not a property of a value here - `Value` has no slot
22 /// for one - it is a property of the *call* that made it, which is
23 /// exactly what the reference records at run time and what the binder
24 /// can see. The one call whose answer depends on the data is
25 /// `json_extract`, which carries the JSON subtype only when what it
26 /// extracted was itself an array or an object; that one is left to run.
27 ///
28 /// @param argument - the call's one argument
29 pub(super) fn bind_subtype(&mut self, argument: ExprId) -> Result<BoundExpr, ParseError> {
30 let bound = self.bind_expr(argument)?;
31 let bound = self.walk_value(&bound).unwrap_or(bound);
32 // A JSON group aggregate carries the subtype too, and its function
33 // is in the binder's list rather than in the expression - so the
34 // slot is resolved here, where the list is.
35 if let BoundExpr::Aggregate { slot, .. } = &bound {
36 let carries = matches!(
37 self.aggregates.get(*slot).map(|held| held.func),
38 Some(
39 function::AggregateFunc::JsonGroupArray
40 | function::AggregateFunc::JsonGroupObject
41 )
42 );
43 return Ok(BoundExpr::Integer(if carries { 74 } else { 0 }));
44 }
45 Ok(match json_subtype(&bound) {
46 Subtyped::Always => BoundExpr::Integer(74),
47 Subtyped::Never => BoundExpr::Integer(0),
48 Subtyped::WhenShaped => BoundExpr::Function {
49 func: function::ScalarFunc::Subtype,
50 arguments: vec![bound],
51 collation: Collation::Binary,
52 },
53 })
54 }
55
56 /// Returns a JSON function's argument with its JSON subtype made visible.
57 ///
58 /// **A subtype reaches the call above only through a nested JSON call.**
59 /// The executor carries the mark from one JSON call to the one that reads
60 /// it, and an argument of any other shape arrives as a plain value. Two
61 /// shapes carry the mark in SQLite and are not JSON calls here:
62 ///
63 /// - a `json_group_array` or `json_group_object` result. `json_object('items',
64 /// json_group_array(item))` answered `{"items":"[\"Latte\"]"}`, the array
65 /// quoted as a string, where SQLite answers `{"items":["Latte"]}`.
66 /// - a scalar subquery whose one column is either of those or a JSON call.
67 /// SQLite keeps the subtype through a scalar subquery, and does not keep
68 /// it through a derived table or a CTE, which this leaves alone.
69 ///
70 /// Each is wrapped in `json()`, or `jsonb()` for the binary aggregates,
71 /// which gives back the same document with the mark on it. Measured
72 /// against 3.53.4, including the cases where SQLite quotes the value.
73 ///
74 /// @param argument - the argument, bound
75 pub(super) fn marked_as_json(&self, argument: BoundExpr) -> BoundExpr {
76 if let Some(walked) = self.walk_value(&argument) {
77 return walked;
78 }
79 let wrapper = match &argument {
80 BoundExpr::Aggregate { slot, .. } => {
81 json_aggregate_wrapper(self.aggregates.get(*slot).map(|held| held.func))
82 }
83 BoundExpr::Subquery {
84 kind: SubqueryKind::Scalar,
85 block,
86 ..
87 } => match block.columns.as_slice() {
88 [only] => match &only.expr {
89 BoundExpr::Aggregate { slot, .. } => {
90 json_aggregate_wrapper(block.aggregates.get(*slot).map(|held| held.func))
91 }
92 other if json_subtype(other) == Subtyped::Always => {
93 Some(function::JsonFunc::Json)
94 }
95 _ => None,
96 },
97 _ => None,
98 },
99 _ => None,
100 };
101 match wrapper {
102 Some(func) => BoundExpr::Json {
103 func,
104 arguments: vec![argument],
105 },
106 None => argument,
107 }
108 }
109}
110
111impl Binder<'_> {
112 /// Returns a `json_each` or `json_tree` row's `value` wrapped so that it
113 /// carries the JSON mark when the row is an array or an object, or `None`
114 /// for any other expression.
115 ///
116 /// See `JsonFunc::WalkValue`. The row's `type` column is the second
117 /// argument; it reads the same term, so it costs one more column of a row
118 /// the scan already produced.
119 ///
120 /// @param argument - an argument of a JSON function, bound
121 pub(super) fn walk_value(&self, argument: &BoundExpr) -> Option<BoundExpr> {
122 const VALUE: u16 = 1;
123 const TYPE: u16 = 2;
124 let BoundExpr::Column {
125 source,
126 column: VALUE,
127 ..
128 } = argument
129 else {
130 return None;
131 };
132 let term = self.sources.iter().find(|term| term.id == *source)?;
133 let walks = term.table.kind == crate::catalog_view::TableKind::Virtual
134 && matches!(term.table.folded.as_slice(), b"json_each" | b"json_tree");
135 if !walks {
136 return None;
137 }
138 let kind = BoundExpr::Column {
139 source: *source,
140 column: TYPE,
141 slot: TYPE,
142 affinity: inillucent_value::affinity::Affinity::Blob,
143 collation: Collation::Binary,
144 };
145 Some(BoundExpr::Json {
146 func: function::JsonFunc::WalkValue,
147 arguments: vec![argument.clone(), kind],
148 })
149 }
150}
151
152/// Returns the JSON call that marks a JSON group aggregate's result, if it is one.
153///
154/// @param func - the aggregate, when the slot named one
155fn json_aggregate_wrapper(func: Option<function::AggregateFunc>) -> Option<function::JsonFunc> {
156 match func? {
157 function::AggregateFunc::JsonGroupArray | function::AggregateFunc::JsonGroupObject => {
158 Some(function::JsonFunc::Json)
159 }
160 function::AggregateFunc::JsonbGroupArray | function::AggregateFunc::JsonbGroupObject => {
161 Some(function::JsonFunc::Jsonb)
162 }
163 _ => None,
164 }
165}
166
167/// Whether a bound expression carries the JSON subtype.
168///
169/// SQLite marks a value with the subtype `74` - the letter `J` - when it was
170/// produced by a function that returns JSON *text*. The binary spellings do
171/// not carry it (a `jsonb_` result is a blob, and a blob read back out of a
172/// column has no subtype either), and the functions that answer a number or a
173/// type name are not JSON at all.
174#[derive(Clone, Copy, Debug, PartialEq, Eq)]
175enum Subtyped {
176 /// The call always marks its answer.
177 Always,
178 /// The call never does.
179 Never,
180 /// It depends on what came out: `json_extract` marks an array or an
181 /// object and does not mark the scalar it may equally have found.
182 WhenShaped,
183}
184
185/// Returns whether an expression's value carries the JSON subtype.
186///
187/// @param bound - the argument to `subtype`
188fn json_subtype(bound: &BoundExpr) -> Subtyped {
189 let BoundExpr::Json { func, .. } = bound else {
190 return Subtyped::Never;
191 };
192 use function::JsonFunc;
193 match func {
194 JsonFunc::Extract | JsonFunc::Arrow | JsonFunc::WalkValue => Subtyped::WhenShaped,
195 JsonFunc::Jsonb
196 | JsonFunc::ArrayB
197 | JsonFunc::ExtractB
198 | JsonFunc::InsertB
199 | JsonFunc::ObjectB
200 | JsonFunc::PatchB
201 | JsonFunc::RemoveB
202 | JsonFunc::ReplaceB
203 | JsonFunc::SetB
204 | JsonFunc::ArrayInsertB
205 | JsonFunc::ArrowShift
206 | JsonFunc::ArrayLength
207 | JsonFunc::ErrorPosition
208 | JsonFunc::Type
209 | JsonFunc::Valid
210 | JsonFunc::Pretty => Subtyped::Never,
211 _ => Subtyped::Always,
212 }
213}
214
215/// Reports whether an expression's value always carries the JSON subtype.
216///
217/// For a caller that reads the value after it has left the expression, such
218/// as a window pass that computes each argument into a column first and so
219/// cannot ask the producing call at run time.
220///
221/// @param bound - the expression
222pub fn always_json(bound: &BoundExpr) -> bool {
223 json_subtype(bound) == Subtyped::Always
224}