mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-20 18:04:19 +01:00
Merge branch 'main' into json-error-position
This commit is contained in:
15
.github/shared/install_sqlite/action.yml
vendored
Normal file
15
.github/shared/install_sqlite/action.yml
vendored
Normal 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
|
||||
36
.github/workflows/rust.yml
vendored
36
.github/workflows/rust.yml
vendored
@@ -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
|
||||
|
||||
@@ -266,8 +266,8 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html).
|
||||
| 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 |
|
||||
| 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,...) | | |
|
||||
|
||||
@@ -19,7 +19,7 @@ version = "0.0.11"
|
||||
authors = ["the Limbo authors"]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/penberg/limbo"
|
||||
repository = "https://github.com/tursodatabase/limbo"
|
||||
|
||||
# Config for 'cargo dist'
|
||||
[workspace.metadata.dist]
|
||||
|
||||
@@ -81,7 +81,8 @@ impl<'a> ImportFile<'a> {
|
||||
|
||||
for r in record.iter() {
|
||||
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("',");
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,10 @@ impl Display for ExternalFunc {
|
||||
pub enum JsonFunc {
|
||||
Json,
|
||||
JsonArray,
|
||||
JsonExtract,
|
||||
JsonArrayLength,
|
||||
JsonArrowExtract,
|
||||
JsonArrowShiftExtract,
|
||||
JsonExtract,
|
||||
JsonType,
|
||||
JsonErrorPosition,
|
||||
}
|
||||
@@ -42,6 +44,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(),
|
||||
Self::JsonErrorPosition => "json_error_position".to_string(),
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
142
core/json/mod.rs
142
core/json/mod.rs
@@ -7,7 +7,7 @@ use std::rc::Rc;
|
||||
|
||||
pub use crate::json::de::from_str;
|
||||
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;
|
||||
use crate::types::{LimboText, OwnedValue, TextSubtype};
|
||||
use indexmap::IndexMap;
|
||||
@@ -126,7 +126,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),
|
||||
}
|
||||
@@ -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> {
|
||||
if let OwnedValue::Null = value {
|
||||
return Ok(OwnedValue::Null);
|
||||
@@ -148,14 +186,15 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result<O
|
||||
|
||||
if paths.is_empty() {
|
||||
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 mut result = "".to_string();
|
||||
|
||||
if paths.len() > 1 {
|
||||
result.push('[');
|
||||
}
|
||||
let mut result = "[".to_string();
|
||||
|
||||
for path in paths {
|
||||
match path {
|
||||
@@ -163,28 +202,59 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result<O
|
||||
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 {
|
||||
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<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> {
|
||||
if let OwnedValue::Null = value {
|
||||
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 = 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),
|
||||
}
|
||||
@@ -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.
|
||||
/// 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 {
|
||||
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<Option<&'a Val>> {
|
||||
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;
|
||||
|
||||
@@ -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(
|
||||
program: &mut ProgramBuilder,
|
||||
referenced_tables: &[TableReference],
|
||||
@@ -413,9 +481,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 +615,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 +776,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,
|
||||
)
|
||||
}
|
||||
JsonFunc::JsonErrorPosition => {
|
||||
let args = if let Some(args) = args {
|
||||
@@ -831,37 +859,16 @@ 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 {
|
||||
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()
|
||||
);
|
||||
};
|
||||
let args = expect_arguments_min!(args, 2, srf);
|
||||
|
||||
// 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
|
||||
@@ -919,17 +926,7 @@ pub fn translate_expr(
|
||||
Ok(target_register)
|
||||
}
|
||||
ScalarFunc::ConcatWs => {
|
||||
let args = match args {
|
||||
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 args = expect_arguments_min!(args, 2, srf);
|
||||
|
||||
let temp_register = program.alloc_register();
|
||||
for arg in args.iter() {
|
||||
@@ -1076,20 +1073,7 @@ pub fn translate_expr(
|
||||
| ScalarFunc::Sign
|
||||
| ScalarFunc::Soundex
|
||||
| ScalarFunc::ZeroBlob => {
|
||||
let args = if let Some(args) = args {
|
||||
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 args = expect_arguments_exact!(args, 1, srf);
|
||||
let reg =
|
||||
translate_and_mark(program, referenced_tables, &args[0], resolver)?;
|
||||
program.emit_insn(Insn::Function {
|
||||
@@ -1275,20 +1259,7 @@ pub fn translate_expr(
|
||||
| ScalarFunc::RTrim
|
||||
| ScalarFunc::Round
|
||||
| ScalarFunc::Unhex => {
|
||||
let args = if let Some(args) = args {
|
||||
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()
|
||||
);
|
||||
};
|
||||
let args = expect_arguments_max!(args, 2, srf);
|
||||
|
||||
for arg in args.iter() {
|
||||
translate_and_mark(program, referenced_tables, arg, resolver)?;
|
||||
@@ -1461,21 +1432,7 @@ pub fn translate_expr(
|
||||
#[cfg(feature = "uuid")]
|
||||
ExtFunc::Uuid(ref uuid_fn) => match uuid_fn {
|
||||
UuidFunc::UuidStr | UuidFunc::UuidBlob | UuidFunc::Uuid7TS => {
|
||||
let args = if let Some(args) = args {
|
||||
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 args = expect_arguments_exact!(args, 1, ext_func);
|
||||
let regs = program.alloc_register();
|
||||
translate_expr(program, referenced_tables, &args[0], regs, resolver)?;
|
||||
program.emit_insn(Insn::Function {
|
||||
@@ -1503,14 +1460,7 @@ pub fn translate_expr(
|
||||
Ok(target_register)
|
||||
}
|
||||
UuidFunc::Uuid7 => {
|
||||
let args = match args {
|
||||
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 args = expect_arguments_max!(args, 1, ext_func);
|
||||
let mut start_reg = None;
|
||||
if let Some(arg) = args.first() {
|
||||
start_reg = Some(translate_and_mark(
|
||||
@@ -1548,18 +1498,7 @@ pub fn translate_expr(
|
||||
}
|
||||
|
||||
MathFuncArity::Unary => {
|
||||
let args = if let Some(args) = args {
|
||||
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 args = expect_arguments_exact!(args, 1, math_func);
|
||||
let reg =
|
||||
translate_and_mark(program, referenced_tables, &args[0], resolver)?;
|
||||
program.emit_insn(Insn::Function {
|
||||
@@ -1572,17 +1511,7 @@ pub fn translate_expr(
|
||||
}
|
||||
|
||||
MathFuncArity::Binary => {
|
||||
let args = if let Some(args) = args {
|
||||
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 args = expect_arguments_exact!(args, 2, math_func);
|
||||
let reg1 = program.alloc_register();
|
||||
let _ =
|
||||
translate_expr(program, referenced_tables, &args[0], reg1, resolver)?;
|
||||
@@ -1599,17 +1528,7 @@ pub fn translate_expr(
|
||||
}
|
||||
|
||||
MathFuncArity::UnaryOrBinary => {
|
||||
let args = if let Some(args) = args {
|
||||
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 args = expect_arguments_max!(args, 2, math_func);
|
||||
|
||||
let regs = program.alloc_registers(args.len());
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
@@ -1843,25 +1762,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<Vec<ast::Expr>>,
|
||||
args: &[ast::Expr],
|
||||
referenced_tables: Option<&[TableReference]>,
|
||||
resolver: &Resolver,
|
||||
target_register: usize,
|
||||
func_ctx: FuncCtx,
|
||||
) -> Result<usize> {
|
||||
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(
|
||||
|
||||
@@ -40,8 +40,9 @@ use crate::util::parse_schema_rows;
|
||||
use crate::vdbe::insn::Insn;
|
||||
#[cfg(feature = "json")]
|
||||
use crate::{
|
||||
function::JsonFunc,
|
||||
json::{get_json, json_array, json_array_length, json_error_position, json_extract, json_type},
|
||||
function::JsonFunc, json::get_json, json::json_array, json::json_array_length,
|
||||
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 datetime::{exec_date, exec_datetime_full, exec_julianday, exec_time, exec_unixepoch};
|
||||
@@ -1381,6 +1382,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),
|
||||
) => {
|
||||
|
||||
@@ -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 }', '$.\"')
|
||||
|
||||
@@ -253,10 +253,10 @@ def test_import_csv(test_name: str, options: str, import_output: str, table_outp
|
||||
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',
|
||||
'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')
|
||||
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
1,2.0,"String1"
|
||||
1,2.0,"String'1"
|
||||
3,4.0,"String2"
|
||||
|
Reference in New Issue
Block a user