Merge branch 'main' into json-error-position

This commit is contained in:
Pekka Enberg
2025-01-13 18:21:37 +02:00
committed by GitHub
13 changed files with 543 additions and 273 deletions

View File

@@ -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

View File

@@ -66,8 +66,6 @@ jobs:
test-limbo: test-limbo:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Install sqlite
run: sudo apt update && sudo apt install -y sqlite3 libsqlite3-dev
- name: Install cargo-c - name: Install cargo-c
env: env:
LINK: https://github.com/lu-zero/cargo-c/releases/download/v0.10.7 LINK: https://github.com/lu-zero/cargo-c/releases/download/v0.10.7
@@ -76,6 +74,7 @@ jobs:
curl -L $LINK/$CARGO_C_FILE | tar xz -C ~/.cargo/bin curl -L $LINK/$CARGO_C_FILE | tar xz -C ~/.cargo/bin
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: "./.github/shared/install_sqlite"
- name: Test - name: Test
run: make test run: make test
@@ -83,7 +82,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Install sqlite - uses: "./.github/shared/install_sqlite"
run: sudo apt update && sudo apt install -y sqlite3 libsqlite3-dev
- name: Test - name: Test
run: SQLITE_EXEC="sqlite3" make test-compat run: SQLITE_EXEC="sqlite3" make test-compat

View File

@@ -266,8 +266,8 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html).
| json_error_position(json) | Yes | | | json_error_position(json) | Yes | |
| 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 | | 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,...) | | | | jsonb_extract(json,path,...) | | |
| json -> path | | | | json -> path | Yes | |
| json ->> path | | | | json ->> path | Yes | |
| json_insert(json,path,value,...) | | | | json_insert(json,path,value,...) | | |
| jsonb_insert(json,path,value,...) | | | | jsonb_insert(json,path,value,...) | | |
| json_object(label1,value1,...) | | | | json_object(label1,value1,...) | | |

View File

@@ -19,7 +19,7 @@ version = "0.0.11"
authors = ["the Limbo authors"] authors = ["the Limbo authors"]
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/penberg/limbo" repository = "https://github.com/tursodatabase/limbo"
# Config for 'cargo dist' # Config for 'cargo dist'
[workspace.metadata.dist] [workspace.metadata.dist]

View File

@@ -81,7 +81,8 @@ impl<'a> ImportFile<'a> {
for r in record.iter() { for r in record.iter() {
values_string.push('\''); values_string.push('\'');
values_string.push_str(r); // The string can have a single quote which needs to be escaped
values_string.push_str(&r.replace("'", "''"));
values_string.push_str("',"); values_string.push_str("',");
} }

View File

@@ -25,8 +25,10 @@ impl Display for ExternalFunc {
pub enum JsonFunc { pub enum JsonFunc {
Json, Json,
JsonArray, JsonArray,
JsonExtract,
JsonArrayLength, JsonArrayLength,
JsonArrowExtract,
JsonArrowShiftExtract,
JsonExtract,
JsonType, JsonType,
JsonErrorPosition, JsonErrorPosition,
} }
@@ -42,6 +44,8 @@ impl Display for JsonFunc {
Self::JsonArray => "json_array".to_string(), Self::JsonArray => "json_array".to_string(),
Self::JsonExtract => "json_extract".to_string(), Self::JsonExtract => "json_extract".to_string(),
Self::JsonArrayLength => "json_array_length".to_string(), Self::JsonArrayLength => "json_array_length".to_string(),
Self::JsonArrowExtract => "->".to_string(),
Self::JsonArrowShiftExtract => "->>".to_string(),
Self::JsonType => "json_type".to_string(), Self::JsonType => "json_type".to_string(),
Self::JsonErrorPosition => "json_error_position".to_string(), Self::JsonErrorPosition => "json_error_position".to_string(),
} }

View File

@@ -1,6 +1,7 @@
negative_index_indicator = ${ "#-" } negative_index_indicator = ${ "#-" }
array_offset = ${ ASCII_DIGIT+ } array_offset = ${ ASCII_DIGIT+ }
array_locator = ${ "[" ~ negative_index_indicator? ~ array_offset ~ "]" } array_locator = ${ "[" ~ negative_index_indicator? ~ array_offset ~ "]" }
relaxed_array_locator = ${ negative_index_indicator? ~ array_offset }
root = ${ "$" } root = ${ "$" }
json_path_key = ${ identifier | string } json_path_key = ${ identifier | string }

View File

@@ -7,7 +7,7 @@ use std::rc::Rc;
pub use crate::json::de::from_str; pub use crate::json::de::from_str;
use crate::json::error::Error as JsonError; use crate::json::error::Error as JsonError;
use crate::json::json_path::{json_path, PathElement}; use crate::json::json_path::{json_path, JsonPath, PathElement};
pub use crate::json::ser::to_string; pub use crate::json::ser::to_string;
use crate::types::{LimboText, OwnedValue, TextSubtype}; use crate::types::{LimboText, OwnedValue, TextSubtype};
use indexmap::IndexMap; use indexmap::IndexMap;
@@ -126,7 +126,7 @@ pub fn json_array_length(
let json = get_json_value(json_value)?; let json = get_json_value(json_value)?;
let arr_val = if let Some(path) = json_path { 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, Some(val) => val,
None => return Ok(OwnedValue::Null), None => return Ok(OwnedValue::Null),
} }
@@ -141,6 +141,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<OwnedValue> {
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<OwnedValue> {
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<OwnedValue> { pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result<OwnedValue> {
if let OwnedValue::Null = value { if let OwnedValue::Null = value {
return Ok(OwnedValue::Null); return Ok(OwnedValue::Null);
@@ -148,14 +186,15 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result<O
if paths.is_empty() { if paths.is_empty() {
return Ok(OwnedValue::Null); return Ok(OwnedValue::Null);
} else if paths.len() == 1 {
let json = get_json_value(value)?;
let extracted = json_extract_single(&json, &paths[0], true)?.unwrap_or_else(|| &Val::Null);
return convert_json_to_db_type(&extracted, false);
} }
let json = get_json_value(value)?; let json = get_json_value(value)?;
let mut result = "".to_string(); let mut result = "[".to_string();
if paths.len() > 1 {
result.push('[');
}
for path in paths { for path in paths {
match path { match path {
@@ -163,28 +202,59 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result<O
return Ok(OwnedValue::Null); return Ok(OwnedValue::Null);
} }
_ => { _ => {
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 { if paths.len() == 1 && extracted == &Val::Null {
return Ok(OwnedValue::Null); return Ok(OwnedValue::Null);
} }
result.push_str(&crate::json::to_string(&extracted).unwrap()); 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.pop(); // remove the final comma
result.push(']'); result.push(']');
}
Ok(OwnedValue::Text(LimboText::json(Rc::new(result)))) 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<OwnedValue> {
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<OwnedValue> { pub fn json_type(value: &OwnedValue, path: Option<&OwnedValue>) -> crate::Result<OwnedValue> {
if let OwnedValue::Null = value { if let OwnedValue::Null = value {
return Ok(OwnedValue::Null); return Ok(OwnedValue::Null);
@@ -193,7 +263,7 @@ pub fn json_type(value: &OwnedValue, path: Option<&OwnedValue>) -> crate::Result
let json = get_json_value(value)?; let json = get_json_value(value)?;
let json = if let Some(path) = path { let json = if let Some(path) = path {
match json_extract_single(&json, path)? { match json_extract_single(&json, path, true)? {
Some(val) => val, Some(val) => val,
None => return Ok(OwnedValue::Null), None => return Ok(OwnedValue::Null),
} }
@@ -222,11 +292,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. /// 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. /// If the path is an invalid path, returns an error.
fn json_extract_single<'a>(json: &'a Val, path: &OwnedValue) -> crate::Result<Option<&'a Val>> { ///
let json_path = match path { /// *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<Option<&'a Val>> {
let json_path = if strict {
match path {
OwnedValue::Text(t) => json_path(t.value.as_str())?, OwnedValue::Text(t) => json_path(t.value.as_str())?,
OwnedValue::Null => return Ok(None), OwnedValue::Null => return Ok(None),
_ => crate::bail_constraint_error!("JSON path error near: {:?}", path.to_string()), _ => 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; let mut current_element = &Val::Null;

View File

@@ -61,6 +61,74 @@ 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
}};
}
macro_rules! expect_arguments_min {
(
$args:expr,
$expected_arguments:expr,
$func:ident
) => {{
let args = if let Some(args) = $args {
if args.len() < $expected_arguments {
crate::bail_parse_error!(
"{} function with less 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( pub fn translate_condition_expr(
program: &mut ProgramBuilder, program: &mut ProgramBuilder,
referenced_tables: &[TableReference], referenced_tables: &[TableReference],
@@ -413,9 +481,10 @@ pub fn translate_expr(
match expr { match expr {
ast::Expr::Between { .. } => todo!(), ast::Expr::Between { .. } => todo!(),
ast::Expr::Binary(e1, op, e2) => { 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)?; translate_expr(program, referenced_tables, e1, e1_reg, resolver)?;
let e2_reg = program.alloc_register();
translate_expr(program, referenced_tables, e2, e2_reg, resolver)?; translate_expr(program, referenced_tables, e2, e2_reg, resolver)?;
match op { match op {
@@ -546,6 +615,24 @@ pub fn translate_expr(
dest: target_register, 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), other_unimplemented => todo!("{:?}", other_unimplemented),
} }
Ok(target_register) Ok(target_register)
@@ -689,100 +776,41 @@ pub fn translate_expr(
#[cfg(feature = "json")] #[cfg(feature = "json")]
Func::Json(j) => match j { Func::Json(j) => match j {
JsonFunc::Json => { JsonFunc::Json => {
let args = if let Some(args) = args { let args = expect_arguments_exact!(args, 1, j);
if args.len() != 1 {
crate::bail_parse_error!( translate_function(
"{} 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(
program, program,
args, args,
referenced_tables, referenced_tables,
resolver, resolver,
)?; target_register,
func_ctx,
program.emit_insn(Insn::Function {
constant_mask: 0,
start_reg,
dest: target_register,
func: func_ctx,
});
Ok(target_register)
}
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::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 JsonFunc::JsonArray | JsonFunc::JsonExtract => translate_function(
} else {
crate::bail_parse_error!(
"{} function with no arguments",
j.to_string()
);
};
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, program,
args.as_deref().unwrap_or_default(),
referenced_tables, referenced_tables,
&args[1],
path_reg,
resolver, 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 = expect_arguments_max!(args, 2, j);
program.emit_insn(Insn::Function { translate_function(
constant_mask: 0, program,
start_reg: json_reg, args,
dest: target_register, referenced_tables,
func: func_ctx, resolver,
}); target_register,
Ok(target_register) func_ctx,
)
} }
JsonFunc::JsonErrorPosition => { JsonFunc::JsonErrorPosition => {
let args = if let Some(args) = args { let args = if let Some(args) = args {
@@ -831,37 +859,16 @@ pub fn translate_expr(
}); });
Ok(target_register) Ok(target_register)
} }
ScalarFunc::Char => { ScalarFunc::Char => translate_function(
let start_reg = translate_variable_sized_function_parameter_list(
program, program,
args, args.as_deref().unwrap_or_default(),
referenced_tables, referenced_tables,
resolver, resolver,
)?; target_register,
func_ctx,
program.emit_insn(Insn::Function { ),
constant_mask: 0,
start_reg,
dest: target_register,
func: func_ctx,
});
Ok(target_register)
}
ScalarFunc::Coalesce => { ScalarFunc::Coalesce => {
let args = if let Some(args) = args { let args = expect_arguments_min!(args, 2, srf);
if args.len() < 2 {
crate::bail_parse_error!(
"{} function with less than 2 arguments",
srf.to_string()
);
}
args
} else {
crate::bail_parse_error!(
"{} function with no arguments",
srf.to_string()
);
};
// coalesce function is implemented as a series of not null checks // coalesce function is implemented as a series of not null checks
// whenever a not null check succeeds, we jump to the end of the series // whenever a not null check succeeds, we jump to the end of the series
@@ -919,17 +926,7 @@ pub fn translate_expr(
Ok(target_register) Ok(target_register)
} }
ScalarFunc::ConcatWs => { ScalarFunc::ConcatWs => {
let args = match args { let args = expect_arguments_min!(args, 2, srf);
Some(args) if args.len() >= 2 => args,
Some(_) => crate::bail_parse_error!(
"{} function requires at least 2 arguments",
srf.to_string()
),
None => crate::bail_parse_error!(
"{} function requires arguments",
srf.to_string()
),
};
let temp_register = program.alloc_register(); let temp_register = program.alloc_register();
for arg in args.iter() { for arg in args.iter() {
@@ -1076,20 +1073,7 @@ pub fn translate_expr(
| ScalarFunc::Sign | ScalarFunc::Sign
| ScalarFunc::Soundex | ScalarFunc::Soundex
| ScalarFunc::ZeroBlob => { | ScalarFunc::ZeroBlob => {
let args = if let Some(args) = args { let args = expect_arguments_exact!(args, 1, srf);
if args.len() != 1 {
crate::bail_parse_error!(
"{} function with not exactly 1 argument",
srf.to_string()
);
}
args
} else {
crate::bail_parse_error!(
"{} function with no arguments",
srf.to_string()
);
};
let reg = let reg =
translate_and_mark(program, referenced_tables, &args[0], resolver)?; translate_and_mark(program, referenced_tables, &args[0], resolver)?;
program.emit_insn(Insn::Function { program.emit_insn(Insn::Function {
@@ -1275,20 +1259,7 @@ pub fn translate_expr(
| ScalarFunc::RTrim | ScalarFunc::RTrim
| ScalarFunc::Round | ScalarFunc::Round
| ScalarFunc::Unhex => { | ScalarFunc::Unhex => {
let args = if let Some(args) = args { let args = expect_arguments_max!(args, 2, srf);
if args.len() > 2 {
crate::bail_parse_error!(
"{} function with more than 2 arguments",
srf.to_string()
);
}
args
} else {
crate::bail_parse_error!(
"{} function with no arguments",
srf.to_string()
);
};
for arg in args.iter() { for arg in args.iter() {
translate_and_mark(program, referenced_tables, arg, resolver)?; translate_and_mark(program, referenced_tables, arg, resolver)?;
@@ -1461,21 +1432,7 @@ pub fn translate_expr(
#[cfg(feature = "uuid")] #[cfg(feature = "uuid")]
ExtFunc::Uuid(ref uuid_fn) => match uuid_fn { ExtFunc::Uuid(ref uuid_fn) => match uuid_fn {
UuidFunc::UuidStr | UuidFunc::UuidBlob | UuidFunc::Uuid7TS => { UuidFunc::UuidStr | UuidFunc::UuidBlob | UuidFunc::Uuid7TS => {
let args = if let Some(args) = args { let args = expect_arguments_exact!(args, 1, ext_func);
if args.len() != 1 {
crate::bail_parse_error!(
"{} function with not exactly 1 argument",
ext_func.to_string()
);
}
args
} else {
crate::bail_parse_error!(
"{} function with no arguments",
ext_func.to_string()
);
};
let regs = program.alloc_register(); let regs = program.alloc_register();
translate_expr(program, referenced_tables, &args[0], regs, resolver)?; translate_expr(program, referenced_tables, &args[0], regs, resolver)?;
program.emit_insn(Insn::Function { program.emit_insn(Insn::Function {
@@ -1503,14 +1460,7 @@ pub fn translate_expr(
Ok(target_register) Ok(target_register)
} }
UuidFunc::Uuid7 => { UuidFunc::Uuid7 => {
let args = match args { let args = expect_arguments_max!(args, 1, ext_func);
Some(args) if args.len() > 1 => crate::bail_parse_error!(
"{} function with more than 1 argument",
ext_func.to_string()
),
Some(args) => args,
None => &vec![],
};
let mut start_reg = None; let mut start_reg = None;
if let Some(arg) = args.first() { if let Some(arg) = args.first() {
start_reg = Some(translate_and_mark( start_reg = Some(translate_and_mark(
@@ -1548,18 +1498,7 @@ pub fn translate_expr(
} }
MathFuncArity::Unary => { MathFuncArity::Unary => {
let args = if let Some(args) = args { let args = expect_arguments_exact!(args, 1, math_func);
if args.len() != 1 {
crate::bail_parse_error!(
"{} function with not exactly 1 argument",
math_func
);
}
args
} else {
crate::bail_parse_error!("{} function with no arguments", math_func);
};
let reg = let reg =
translate_and_mark(program, referenced_tables, &args[0], resolver)?; translate_and_mark(program, referenced_tables, &args[0], resolver)?;
program.emit_insn(Insn::Function { program.emit_insn(Insn::Function {
@@ -1572,17 +1511,7 @@ pub fn translate_expr(
} }
MathFuncArity::Binary => { MathFuncArity::Binary => {
let args = if let Some(args) = args { let args = expect_arguments_exact!(args, 2, math_func);
if args.len() != 2 {
crate::bail_parse_error!(
"{} function with not exactly 2 arguments",
math_func
);
}
args
} else {
crate::bail_parse_error!("{} function with no arguments", math_func);
};
let reg1 = program.alloc_register(); let reg1 = program.alloc_register();
let _ = let _ =
translate_expr(program, referenced_tables, &args[0], reg1, resolver)?; translate_expr(program, referenced_tables, &args[0], reg1, resolver)?;
@@ -1599,17 +1528,7 @@ pub fn translate_expr(
} }
MathFuncArity::UnaryOrBinary => { MathFuncArity::UnaryOrBinary => {
let args = if let Some(args) = args { let args = expect_arguments_max!(args, 2, math_func);
if args.len() > 2 {
crate::bail_parse_error!(
"{} function with more than 2 arguments",
math_func
);
}
args
} else {
crate::bail_parse_error!("{} function with no arguments", math_func);
};
let regs = program.alloc_registers(args.len()); let regs = program.alloc_registers(args.len());
for (i, arg) in args.iter().enumerate() { for (i, arg) in args.iter().enumerate() {
@@ -1843,25 +1762,33 @@ pub fn translate_expr(
} }
} }
// Returns the starting register for the function. /// Emits a whole insn for a function call.
// TODO: Use this function for all functions with variable number of parameters in `translate_expr` /// Assumes the number of parameters is valid for the given function.
fn translate_variable_sized_function_parameter_list( /// Returns the target register for the function.
fn translate_function(
program: &mut ProgramBuilder, program: &mut ProgramBuilder,
args: &Option<Vec<ast::Expr>>, args: &[ast::Expr],
referenced_tables: Option<&[TableReference]>, referenced_tables: Option<&[TableReference]>,
resolver: &Resolver, resolver: &Resolver,
target_register: usize,
func_ctx: FuncCtx,
) -> Result<usize> { ) -> Result<usize> {
let args = args.as_deref().unwrap_or_default(); let start_reg = program.alloc_registers(args.len());
let mut current_reg = start_reg;
let reg = program.alloc_registers(args.len());
let mut current_reg = reg;
for arg in args.iter() { for arg in args.iter() {
translate_expr(program, referenced_tables, arg, current_reg, resolver)?; translate_expr(program, referenced_tables, arg, current_reg, resolver)?;
current_reg += 1; 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( fn wrap_eval_jump_expr(

View File

@@ -40,8 +40,9 @@ use crate::util::parse_schema_rows;
use crate::vdbe::insn::Insn; use crate::vdbe::insn::Insn;
#[cfg(feature = "json")] #[cfg(feature = "json")]
use crate::{ use crate::{
function::JsonFunc, function::JsonFunc, json::get_json, json::json_array, json::json_array_length,
json::{get_json, json_array, json_array_length, json_error_position, json_extract, json_type}, json::json_arrow_extract, json::json_arrow_shift_extract, json::json_extract, json::json_type,
json::json_error_position,
}; };
use crate::{Connection, Result, Rows, TransactionState, DATABASE_VERSION}; use crate::{Connection, Result, Rows, TransactionState, DATABASE_VERSION};
use datetime::{exec_date, exec_datetime_full, exec_julianday, exec_time, exec_unixepoch}; use datetime::{exec_date, exec_datetime_full, exec_julianday, exec_time, exec_unixepoch};
@@ -1381,6 +1382,24 @@ impl Program {
} }
} }
#[cfg(feature = "json")] #[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( crate::function::Func::Json(
func @ (JsonFunc::JsonArrayLength | JsonFunc::JsonType), func @ (JsonFunc::JsonArrayLength | JsonFunc::JsonType),
) => { ) => {

View File

@@ -89,6 +89,18 @@ do_execsql_test json_extract_null {
SELECT 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 { do_execsql_test json_extract_empty {
SELECT json_extract() SELECT json_extract()
} {{}} } {{}}
@@ -113,10 +125,38 @@ do_execsql_test json_extract_number {
SELECT json_extract(1, '$') SELECT json_extract(1, '$')
} {{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 { do_execsql_test json_extract_object_1 {
SELECT json_extract('{"a": [1,2,3]}', '$.a') SELECT json_extract('{"a": [1,2,3]}', '$.a')
} {{[1,2,3]}} } {{[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 { do_execsql_test json_extract_object_2 {
SELECT json_extract('{"a": [1,2,3]}', '$.a', '$.a[0]', '$.a[1]', '$.a[3]') SELECT json_extract('{"a": [1,2,3]}', '$.a', '$.a[0]', '$.a[1]', '$.a[3]')
} {{[[1,2,3],1,2,null]}} } {{[[1,2,3],1,2,null]}}
@@ -140,11 +180,176 @@ do_execsql_test json_extract_null_path {
SELECT json_extract(1, null) 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. # TODO: fix me - this passes on SQLite and needs to be fixed in Limbo.
do_execsql_test json_extract_multiple_null_paths { do_execsql_test json_extract_multiple_null_paths {
SELECT json_extract(1, null, null, null) 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. # TODO: fix me - this passes on SQLite and needs to be fixed in Limbo.
#do_execsql_test json_extract_quote { #do_execsql_test json_extract_quote {
# SELECT json_extract('{"\"":1 }', '$.\"') # SELECT json_extract('{"\"":1 }', '$.\"')

View File

@@ -253,10 +253,10 @@ def test_import_csv(test_name: str, options: str, import_output: str, table_outp
table_output, table_output,
) )
test_import_csv('no_options', '--csv', '', '1|2.0|String1\n3|4.0|String2') test_import_csv('no_options', '--csv', '', '1|2.0|String\'1\n3|4.0|String2')
test_import_csv('verbose', '--csv -v', test_import_csv('verbose', '--csv -v',
'Added 2 rows with 0 errors using 2 lines of input' 'Added 2 rows with 0 errors using 2 lines of input'
,'1|2.0|String1\n3|4.0|String2') ,'1|2.0|String\'1\n3|4.0|String2')
test_import_csv('skip', '--csv --skip 1', '' ,'3|4.0|String2') test_import_csv('skip', '--csv --skip 1', '' ,'3|4.0|String2')

View File

@@ -1,2 +1,2 @@
1,2.0,"String1" 1,2.0,"String'1"
3,4.0,"String2" 3,4.0,"String2"
1 1 2.0 String1 String'1
2 3 4.0 String2 String2