Skip to main content

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}