diff --git a/.github/shared/install_sqlite/action.yml b/.github/shared/install_sqlite/action.yml new file mode 100644 index 000000000..f74f620f1 --- /dev/null +++ b/.github/shared/install_sqlite/action.yml @@ -0,0 +1,15 @@ +name: "Install SQLite" +description: "Downloads SQLite directly from https://sqlite.org" + +runs: + using: "composite" + steps: + - name: Install SQLite + env: + SQLITE_VERSION: "3470200" + YEAR: 2024 + run: | + curl -o /tmp/sqlite.zip https://www.sqlite.org/$YEAR/sqlite-tools-linux-x64-$SQLITE_VERSION.zip > /dev/null + unzip -j /tmp/sqlite.zip sqlite3 -d /usr/local/bin/ + sqlite3 --version + shell: bash diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 19596e140..bebf36794 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -59,31 +59,29 @@ jobs: bench: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Bench - run: cargo bench + - uses: actions/checkout@v3 + - name: Bench + run: cargo bench test-limbo: runs-on: ubuntu-latest steps: - - name: Install sqlite - run: sudo apt update && sudo apt install -y sqlite3 libsqlite3-dev - - name: Install cargo-c - env: - LINK: https://github.com/lu-zero/cargo-c/releases/download/v0.10.7 - CARGO_C_FILE: cargo-c-x86_64-unknown-linux-musl.tar.gz - run: | - curl -L $LINK/$CARGO_C_FILE | tar xz -C ~/.cargo/bin + - name: Install cargo-c + env: + LINK: https://github.com/lu-zero/cargo-c/releases/download/v0.10.7 + CARGO_C_FILE: cargo-c-x86_64-unknown-linux-musl.tar.gz + run: | + curl -L $LINK/$CARGO_C_FILE | tar xz -C ~/.cargo/bin - - uses: actions/checkout@v3 - - name: Test - run: make test + - uses: actions/checkout@v3 + - uses: "./.github/shared/install_sqlite" + - name: Test + run: make test test-sqlite: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Install sqlite - run: sudo apt update && sudo apt install -y sqlite3 libsqlite3-dev - - name: Test - run: SQLITE_EXEC="sqlite3" make test-compat + - uses: actions/checkout@v3 + - uses: "./.github/shared/install_sqlite" + - name: Test + run: SQLITE_EXEC="sqlite3" make test-compat diff --git a/COMPAT.md b/COMPAT.md index 4ddabe10f..54949c91d 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -266,8 +266,8 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html). | json_error_position(json) | | | | json_extract(json,path,...) | Partial | Does not fully support unicode literal syntax and does not allow numbers > 2^127 - 1 (which SQLite truncates to i32), does not support BLOBs | | jsonb_extract(json,path,...) | | | -| json -> path | | | -| json ->> path | | | +| json -> path | Yes | | +| json ->> path | Yes | | | json_insert(json,path,value,...) | | | | jsonb_insert(json,path,value,...) | | | | json_object(label1,value1,...) | | | diff --git a/core/function.rs b/core/function.rs index 94d752eb5..7b906406a 100644 --- a/core/function.rs +++ b/core/function.rs @@ -25,8 +25,10 @@ impl Display for ExternalFunc { pub enum JsonFunc { Json, JsonArray, - JsonExtract, JsonArrayLength, + JsonArrowExtract, + JsonArrowShiftExtract, + JsonExtract, JsonType, } @@ -41,6 +43,8 @@ impl Display for JsonFunc { Self::JsonArray => "json_array".to_string(), Self::JsonExtract => "json_extract".to_string(), Self::JsonArrayLength => "json_array_length".to_string(), + Self::JsonArrowExtract => "->".to_string(), + Self::JsonArrowShiftExtract => "->>".to_string(), Self::JsonType => "json_type".to_string(), } ) diff --git a/core/json/json_path.pest b/core/json/json_path.pest index b9a4d3f22..71a462edc 100644 --- a/core/json/json_path.pest +++ b/core/json/json_path.pest @@ -1,6 +1,7 @@ negative_index_indicator = ${ "#-" } array_offset = ${ ASCII_DIGIT+ } array_locator = ${ "[" ~ negative_index_indicator? ~ array_offset ~ "]" } +relaxed_array_locator = ${ negative_index_indicator? ~ array_offset } root = ${ "$" } json_path_key = ${ identifier | string } diff --git a/core/json/mod.rs b/core/json/mod.rs index 8ee36b33a..f0a362ab6 100644 --- a/core/json/mod.rs +++ b/core/json/mod.rs @@ -6,7 +6,7 @@ mod ser; use std::rc::Rc; pub use crate::json::de::from_str; -use crate::json::json_path::{json_path, PathElement}; +use crate::json::json_path::{json_path, JsonPath, PathElement}; pub use crate::json::ser::to_string; use crate::types::{LimboText, OwnedValue, TextSubtype}; use indexmap::IndexMap; @@ -124,7 +124,7 @@ pub fn json_array_length( let json = get_json_value(json_value)?; let arr_val = if let Some(path) = json_path { - match json_extract_single(&json, path)? { + match json_extract_single(&json, path, true)? { Some(val) => val, None => return Ok(OwnedValue::Null), } @@ -139,6 +139,44 @@ pub fn json_array_length( } } +/// Implements the -> operator. Always returns a proper JSON value. +/// https://sqlite.org/json1.html#the_and_operators +pub fn json_arrow_extract(value: &OwnedValue, path: &OwnedValue) -> crate::Result { + if let OwnedValue::Null = value { + return Ok(OwnedValue::Null); + } + + let json = get_json_value(value)?; + let extracted = json_extract_single(&json, path, false)?; + + if let Some(val) = extracted { + let json = crate::json::to_string(val).unwrap(); + + Ok(OwnedValue::Text(LimboText::json(Rc::new(json)))) + } else { + Ok(OwnedValue::Null) + } +} + +/// Implements the ->> operator. Always returns a SQL representation of the JSON subcomponent. +/// https://sqlite.org/json1.html#the_and_operators +pub fn json_arrow_shift_extract( + value: &OwnedValue, + path: &OwnedValue, +) -> crate::Result { + if let OwnedValue::Null = value { + return Ok(OwnedValue::Null); + } + + let json = get_json_value(value)?; + let extracted = json_extract_single(&json, path, false)?.unwrap_or_else(|| &Val::Null); + + convert_json_to_db_type(extracted, true) +} + +/// Extracts a JSON value from a JSON object or array. +/// If there's only a single path, the return value might be either a TEXT or a database type. +/// https://sqlite.org/json1.html#the_json_extract_function pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result { if let OwnedValue::Null = value { return Ok(OwnedValue::Null); @@ -146,14 +184,15 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result 1 { - result.push('['); - } + let mut result = "[".to_string(); for path in paths { match path { @@ -161,28 +200,59 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result { - let extracted = json_extract_single(&json, path)?.unwrap_or_else(|| &Val::Null); + let extracted = + json_extract_single(&json, path, true)?.unwrap_or_else(|| &Val::Null); if paths.len() == 1 && extracted == &Val::Null { return Ok(OwnedValue::Null); } result.push_str(&crate::json::to_string(&extracted).unwrap()); - if paths.len() > 1 { - result.push(','); - } + result.push(','); } } } - if paths.len() > 1 { - result.pop(); // remove the final comma - result.push(']'); - } + result.pop(); // remove the final comma + result.push(']'); Ok(OwnedValue::Text(LimboText::json(Rc::new(result)))) } +/// Returns a value with type defined by SQLite documentation: +/// > the SQL datatype of the result is NULL for a JSON null, +/// > INTEGER or REAL for a JSON numeric value, +/// > an INTEGER zero for a JSON false value, +/// > an INTEGER one for a JSON true value, +/// > the dequoted text for a JSON string value, +/// > and a text representation for JSON object and array values. +/// https://sqlite.org/json1.html#the_json_extract_function +/// +/// *all_as_db* - if true, objects and arrays will be returned as pure TEXT without the JSON subtype +fn convert_json_to_db_type(extracted: &Val, all_as_db: bool) -> crate::Result { + match extracted { + Val::Null => Ok(OwnedValue::Null), + Val::Float(f) => Ok(OwnedValue::Float(*f)), + Val::Integer(i) => Ok(OwnedValue::Integer(*i)), + Val::Bool(b) => { + if *b { + Ok(OwnedValue::Integer(1)) + } else { + Ok(OwnedValue::Integer(0)) + } + } + Val::String(s) => Ok(OwnedValue::Text(LimboText::new(Rc::new(s.clone())))), + _ => { + let json = crate::json::to_string(&extracted).unwrap(); + if all_as_db { + Ok(OwnedValue::Text(LimboText::new(Rc::new(json)))) + } else { + Ok(OwnedValue::Text(LimboText::json(Rc::new(json)))) + } + } + } +} + pub fn json_type(value: &OwnedValue, path: Option<&OwnedValue>) -> crate::Result { if let OwnedValue::Null = value { return Ok(OwnedValue::Null); @@ -191,7 +261,7 @@ pub fn json_type(value: &OwnedValue, path: Option<&OwnedValue>) -> crate::Result let json = get_json_value(value)?; let json = if let Some(path) = path { - match json_extract_single(&json, path)? { + match json_extract_single(&json, path, true)? { Some(val) => val, None => return Ok(OwnedValue::Null), } @@ -220,11 +290,41 @@ pub fn json_type(value: &OwnedValue, path: Option<&OwnedValue>) -> crate::Result /// Returns the value at the given JSON path. If the path does not exist, it returns None. /// If the path is an invalid path, returns an error. -fn json_extract_single<'a>(json: &'a Val, path: &OwnedValue) -> crate::Result> { - let json_path = match path { - OwnedValue::Text(t) => json_path(t.value.as_str())?, - OwnedValue::Null => return Ok(None), - _ => crate::bail_constraint_error!("JSON path error near: {:?}", path.to_string()), +/// +/// *strict* - if false, we will try to resolve the path even if it does not start with "$" +/// in a way that's compatible with the `->` and `->>` operators. See examples in the docs: +/// https://sqlite.org/json1.html#the_and_operators +fn json_extract_single<'a>( + json: &'a Val, + path: &OwnedValue, + strict: bool, +) -> crate::Result> { + let json_path = if strict { + match path { + OwnedValue::Text(t) => json_path(t.value.as_str())?, + OwnedValue::Null => return Ok(None), + _ => crate::bail_constraint_error!("JSON path error near: {:?}", path.to_string()), + } + } else { + match path { + OwnedValue::Text(t) => { + if t.value.starts_with("$") { + json_path(t.value.as_str())? + } else { + JsonPath { + elements: vec![PathElement::Root(), PathElement::Key(t.value.to_string())], + } + } + } + OwnedValue::Null => return Ok(None), + OwnedValue::Integer(i) => JsonPath { + elements: vec![PathElement::Root(), PathElement::ArrayLocator(*i as i32)], + }, + OwnedValue::Float(f) => JsonPath { + elements: vec![PathElement::Root(), PathElement::Key(f.to_string())], + }, + _ => crate::bail_constraint_error!("JSON path error near: {:?}", path.to_string()), + } }; let mut current_element = &Val::Null; diff --git a/core/translate/expr.rs b/core/translate/expr.rs index be4cbf534..c229e8501 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -61,6 +61,52 @@ macro_rules! emit_cmp_insn { }}; } +macro_rules! expect_arguments_exact { + ( + $args:expr, + $expected_arguments:expr, + $func:ident + ) => {{ + let args = if let Some(args) = $args { + if args.len() != $expected_arguments { + crate::bail_parse_error!( + "{} function called with not exactly {} arguments", + $func.to_string(), + $expected_arguments, + ); + } + args + } else { + crate::bail_parse_error!("{} function with no arguments", $func.to_string()); + }; + + args + }}; +} + +macro_rules! expect_arguments_max { + ( + $args:expr, + $expected_arguments:expr, + $func:ident + ) => {{ + let args = if let Some(args) = $args { + if args.len() > $expected_arguments { + crate::bail_parse_error!( + "{} function called with more than {} arguments", + $func.to_string(), + $expected_arguments, + ); + } + args + } else { + crate::bail_parse_error!("{} function with no arguments", $func.to_string()); + }; + + args + }}; +} + pub fn translate_condition_expr( program: &mut ProgramBuilder, referenced_tables: &[TableReference], @@ -413,9 +459,10 @@ pub fn translate_expr( match expr { ast::Expr::Between { .. } => todo!(), ast::Expr::Binary(e1, op, e2) => { - let e1_reg = program.alloc_register(); + let e1_reg = program.alloc_registers(2); + let e2_reg = e1_reg + 1; + translate_expr(program, referenced_tables, e1, e1_reg, resolver)?; - let e2_reg = program.alloc_register(); translate_expr(program, referenced_tables, e2, e2_reg, resolver)?; match op { @@ -546,6 +593,24 @@ pub fn translate_expr( dest: target_register, }); } + #[cfg(feature = "json")] + op @ (ast::Operator::ArrowRight | ast::Operator::ArrowRightShift) => { + let json_func = match op { + ast::Operator::ArrowRight => JsonFunc::JsonArrowExtract, + ast::Operator::ArrowRightShift => JsonFunc::JsonArrowShiftExtract, + _ => unreachable!(), + }; + + program.emit_insn(Insn::Function { + constant_mask: 0, + start_reg: e1_reg, + dest: target_register, + func: FuncCtx { + func: Func::Json(json_func), + arg_count: 2, + }, + }) + } other_unimplemented => todo!("{:?}", other_unimplemented), } Ok(target_register) @@ -689,100 +754,41 @@ pub fn translate_expr( #[cfg(feature = "json")] Func::Json(j) => match j { JsonFunc::Json => { - let args = if let Some(args) = args { - if args.len() != 1 { - crate::bail_parse_error!( - "{} function with not exactly 1 argument", - j.to_string() - ); - } - args - } else { - crate::bail_parse_error!( - "{} function with no arguments", - j.to_string() - ); - }; - let regs = program.alloc_register(); - translate_expr(program, referenced_tables, &args[0], regs, resolver)?; - program.emit_insn(Insn::Function { - constant_mask: 0, - start_reg: regs, - dest: target_register, - func: func_ctx, - }); - Ok(target_register) - } - JsonFunc::JsonArray => { - let start_reg = translate_variable_sized_function_parameter_list( + let args = expect_arguments_exact!(args, 1, j); + + translate_function( program, args, referenced_tables, resolver, - )?; - - program.emit_insn(Insn::Function { - constant_mask: 0, - start_reg, - dest: target_register, - func: func_ctx, - }); - Ok(target_register) + target_register, + func_ctx, + ) } - JsonFunc::JsonExtract => { - let start_reg = translate_variable_sized_function_parameter_list( - program, - args, - referenced_tables, - resolver, - )?; - - program.emit_insn(Insn::Function { - constant_mask: 0, - start_reg, - dest: target_register, - func: func_ctx, - }); - Ok(target_register) + JsonFunc::JsonArray | JsonFunc::JsonExtract => translate_function( + program, + args.as_deref().unwrap_or_default(), + referenced_tables, + resolver, + target_register, + func_ctx, + ), + JsonFunc::JsonArrowExtract | JsonFunc::JsonArrowShiftExtract => { + unreachable!( + "These two functions are only reachable via the -> and ->> operators" + ) } JsonFunc::JsonArrayLength | JsonFunc::JsonType => { - let args = if let Some(args) = args { - if args.len() > 2 { - crate::bail_parse_error!( - "{} function with wrong number of arguments", - j.to_string() - ) - } - args - } else { - crate::bail_parse_error!( - "{} function with no arguments", - j.to_string() - ); - }; + let args = expect_arguments_max!(args, 2, j); - let json_reg = program.alloc_register(); - let path_reg = program.alloc_register(); - - translate_expr(program, referenced_tables, &args[0], json_reg, resolver)?; - - if args.len() == 2 { - translate_expr( - program, - referenced_tables, - &args[1], - path_reg, - resolver, - )?; - } - - program.emit_insn(Insn::Function { - constant_mask: 0, - start_reg: json_reg, - dest: target_register, - func: func_ctx, - }); - Ok(target_register) + translate_function( + program, + args, + referenced_tables, + resolver, + target_register, + func_ctx, + ) } }, Func::Scalar(srf) => { @@ -806,22 +812,14 @@ pub fn translate_expr( }); Ok(target_register) } - ScalarFunc::Char => { - let start_reg = translate_variable_sized_function_parameter_list( - program, - args, - referenced_tables, - resolver, - )?; - - program.emit_insn(Insn::Function { - constant_mask: 0, - start_reg, - dest: target_register, - func: func_ctx, - }); - Ok(target_register) - } + ScalarFunc::Char => translate_function( + program, + args.as_deref().unwrap_or_default(), + referenced_tables, + resolver, + target_register, + func_ctx, + ), ScalarFunc::Coalesce => { let args = if let Some(args) = args { if args.len() < 2 { @@ -1818,25 +1816,33 @@ pub fn translate_expr( } } -// Returns the starting register for the function. -// TODO: Use this function for all functions with variable number of parameters in `translate_expr` -fn translate_variable_sized_function_parameter_list( +/// Emits a whole insn for a function call. +/// Assumes the number of parameters is valid for the given function. +/// Returns the target register for the function. +fn translate_function( program: &mut ProgramBuilder, - args: &Option>, + args: &[ast::Expr], referenced_tables: Option<&[TableReference]>, resolver: &Resolver, + target_register: usize, + func_ctx: FuncCtx, ) -> Result { - let args = args.as_deref().unwrap_or_default(); - - let reg = program.alloc_registers(args.len()); - let mut current_reg = reg; + let start_reg = program.alloc_registers(args.len()); + let mut current_reg = start_reg; for arg in args.iter() { translate_expr(program, referenced_tables, arg, current_reg, resolver)?; current_reg += 1; } - Ok(reg) + program.emit_insn(Insn::Function { + constant_mask: 0, + start_reg, + dest: target_register, + func: func_ctx, + }); + + Ok(target_register) } fn wrap_eval_jump_expr( diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 73557303e..64c41b3bd 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -41,7 +41,7 @@ use crate::vdbe::insn::Insn; #[cfg(feature = "json")] use crate::{ function::JsonFunc, json::get_json, json::json_array, json::json_array_length, - json::json_extract, json::json_type, + json::json_arrow_extract, json::json_arrow_shift_extract, json::json_extract, json::json_type, }; use crate::{Connection, Result, Rows, TransactionState, DATABASE_VERSION}; use datetime::{exec_date, exec_datetime_full, exec_julianday, exec_time, exec_unixepoch}; @@ -1381,6 +1381,24 @@ impl Program { } } #[cfg(feature = "json")] + crate::function::Func::Json( + func @ (JsonFunc::JsonArrowExtract | JsonFunc::JsonArrowShiftExtract), + ) => { + assert_eq!(arg_count, 2); + let json = &state.registers[*start_reg]; + let path = &state.registers[*start_reg + 1]; + let func = match func { + JsonFunc::JsonArrowExtract => json_arrow_extract, + JsonFunc::JsonArrowShiftExtract => json_arrow_shift_extract, + _ => unreachable!(), + }; + let json_str = func(json, path); + match json_str { + Ok(json) => state.registers[*dest] = json, + Err(e) => return Err(e), + } + } + #[cfg(feature = "json")] crate::function::Func::Json( func @ (JsonFunc::JsonArrayLength | JsonFunc::JsonType), ) => { diff --git a/testing/json.test b/testing/json.test index fd2cdb54c..a4df0e902 100755 --- a/testing/json.test +++ b/testing/json.test @@ -89,6 +89,18 @@ do_execsql_test json_extract_null { SELECT json_extract(null, '$') } {{}} +do_execsql_test json_extract_json_null_type { + SELECT typeof(json_extract('null', '$')) +} {{null}} + +do_execsql_test json_arrow_json_null_type { + SELECT typeof('null' -> '$') +} {{text}} + +do_execsql_test json_arrow_shift_json_null_type { + SELECT typeof('null' ->> '$') +} {{null}} + do_execsql_test json_extract_empty { SELECT json_extract() } {{}} @@ -113,10 +125,38 @@ do_execsql_test json_extract_number { SELECT json_extract(1, '$') } {{1}} +do_execsql_test json_extract_number_type { + SELECT typeof(json_extract(1, '$')) +} {{integer}} + +do_execsql_test json_arrow_number { + SELECT 1 -> '$' +} {{1}} + +do_execsql_test json_arrow_number_type { + SELECT typeof(1 -> '$') +} {{text}} + +do_execsql_test json_arrow_shift_number { + SELECT 1 -> '$' +} {{1}} + +do_execsql_test json_arrow_shift_number_type { + SELECT typeof(1 ->> '$') +} {{integer}} + do_execsql_test json_extract_object_1 { SELECT json_extract('{"a": [1,2,3]}', '$.a') } {{[1,2,3]}} +do_execsql_test json_arrow_object { + SELECT '{"a": [1,2,3]}' -> '$.a' +} {{[1,2,3]}} + +do_execsql_test json_arrow_shift_object { + SELECT '{"a": [1,2,3]}' ->> '$.a' +} {{[1,2,3]}} + do_execsql_test json_extract_object_2 { SELECT json_extract('{"a": [1,2,3]}', '$.a', '$.a[0]', '$.a[1]', '$.a[3]') } {{[[1,2,3],1,2,null]}} @@ -140,11 +180,176 @@ do_execsql_test json_extract_null_path { SELECT json_extract(1, null) } {{}} +do_execsql_test json_arrow_null_path { + SELECT 1 -> null +} {{}} + +do_execsql_test json_arrow_shift_null_path { + SELECT 1 ->> null +} {{}} + +do_execsql_test json_extract_float { + SELECT typeof(json_extract(1.0, '$')) +} {{real}} + +do_execsql_test json_arrow_float { + SELECT typeof(1.0 -> '$') +} {{text}} + +do_execsql_test json_arrow_shift_float { + SELECT typeof(1.0 ->> '$') +} {{real}} + +do_execsql_test json_extract_true { + SELECT json_extract('true', '$') +} {{1}} + +do_execsql_test json_extract_true_type { + SELECT typeof(json_extract('true', '$')) +} {{integer}} + +do_execsql_test json_arrow_true { + SELECT 'true' -> '$' +} {{true}} + +do_execsql_test json_arrow_true_type { + SELECT typeof('true' -> '$') +} {{text}} + +do_execsql_test json_arrow_shift_true { + SELECT 'true' ->> '$' +} {{1}} + +do_execsql_test json_arrow_shift_true_type { + SELECT typeof('true' ->> '$') +} {{integer}} + +do_execsql_test json_extract_false { + SELECT json_extract('false', '$') +} {{0}} + +do_execsql_test json_extract_false_type { + SELECT typeof(json_extract('false', '$')) +} {{integer}} + +do_execsql_test json_arrow_false { + SELECT 'false' -> '$' +} {{false}} + +do_execsql_test json_arrow_false_type { + SELECT typeof('false' -> '$') +} {{text}} + +do_execsql_test json_arrow_shift_false { + SELECT 'false' ->> '$' +} {{0}} + +do_execsql_test json_arrow_shift_false_type { + SELECT typeof('false' ->> '$') +} {{integer}} + +do_execsql_test json_extract_string { + SELECT json_extract('"string"', '$') +} {{string}} + +do_execsql_test json_extract_string_type { + SELECT typeof(json_extract('"string"', '$')) +} {{text}} + +do_execsql_test json_arrow_string { + SELECT '"string"' -> '$' +} {{"string"}} + +do_execsql_test json_arrow_string_type { + SELECT typeof('"string"' -> '$') +} {{text}} + +do_execsql_test json_arrow_shift_string { + SELECT '"string"' ->> '$' +} {{string}} + +do_execsql_test json_arrow_shift_string_type { + SELECT typeof('"string"' ->> '$') +} {{text}} + +do_execsql_test json_arrow_implicit_root_path { + SELECT '{"a":1}' -> 'a'; +} {{1}} + +do_execsql_test json_arrow_shift_implicit_root_path { + SELECT '{"a":1}' ->> 'a'; +} {{1}} + +# TODO: fix me after rebasing on top of https://github.com/tursodatabase/limbo/pull/631 - use the Option value in json_extract_single +#do_execsql_test json_arrow_implicit_root_path_undefined_key { +# SELECT '{"a":1}' -> 'x'; +#} {{}} + +do_execsql_test json_arrow_shift_implicit_root_path_undefined_key { + SELECT '{"a":1}' ->> 'x'; +} {{}} + +do_execsql_test json_arrow_implicit_root_path_array { + SELECT '[1,2,3]' -> 1; +} {{2}} + +do_execsql_test json_arrow_shift_implicit_root_path_array { + SELECT '[1,2,3]' ->> 1; +} {{2}} + +do_execsql_test json_arrow_implicit_root_path_array_negative_idx { + SELECT '[1,2,3]' -> -1; +} {{3}} + +do_execsql_test json_arrow_shift_implicit_root_path_array_negative_idx { + SELECT '[1,2,3]' ->> -1; +} {{3}} + +do_execsql_test json_arrow_implicit_real_cast { + SELECT '{"1.5":"abc"}' -> 1.5; +} {{"abc"}} + +do_execsql_test json_arrow_shift_implicit_real_cast { + SELECT '{"1.5":"abc"}' -> 1.5; +} {{"abc"}} + +do_execsql_test json_arrow_implicit_true_cast { + SELECT '[1,2,3]' -> true +} {{2}} + +do_execsql_test json_arrow_shift_implicit_true_cast { + SELECT '[1,2,3]' ->> true +} {{2}} + +do_execsql_test json_arrow_implicit_false_cast { + SELECT '[1,2,3]' -> false +} {{1}} + +do_execsql_test json_arrow_shift_implicit_false_cast { + SELECT '[1,2,3]' ->> false +} {{1}} + +do_execsql_test json_arrow_chained { + select '{"a":2,"c":[4,5,{"f":7}]}' -> 'c' -> 2 ->> 'f' +} {{7}} + # TODO: fix me - this passes on SQLite and needs to be fixed in Limbo. do_execsql_test json_extract_multiple_null_paths { SELECT json_extract(1, null, null, null) } {{}} +do_execsql_test json_extract_array { + SELECT json_extract('[1,2,3]', '$') +} {{[1,2,3]}} + +do_execsql_test json_arrow_array { + SELECT '[1,2,3]' -> '$' +} {{[1,2,3]}} + +do_execsql_test json_arrow_shift_array { + SELECT '[1,2,3]' ->> '$' +} {{[1,2,3]}} + # TODO: fix me - this passes on SQLite and needs to be fixed in Limbo. #do_execsql_test json_extract_quote { # SELECT json_extract('{"\"":1 }', '$.\"')