Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for VALUES statement #1049

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/translate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub(crate) mod result_row;
pub(crate) mod select;
pub(crate) mod subquery;
pub(crate) mod transaction;
pub(crate) mod values;

use crate::schema::Schema;
use crate::storage::pager::Pager;
Expand Down
56 changes: 50 additions & 6 deletions core/translate/select.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::emitter::emit_program;
use super::plan::{select_star, Operation, Search, SelectQueryType};
use super::planner::Scope;
use super::values::emit_values;
use crate::function::{AggFunc, ExtFunc, Func};
use crate::translate::optimizer::optimize_plan;
use crate::translate::plan::{Aggregate, Direction, GroupBy, Plan, ResultSetColumn, SelectPlan};
Expand All @@ -21,18 +22,25 @@ pub fn translate_select(
select: ast::Select,
syms: &SymbolTable,
) -> Result<ProgramBuilder> {
let mut select_plan = prepare_select_plan(schema, select, syms, None)?;
let mut select_plan = prepare_select_plan(schema, select.clone(), syms, None)?;
optimize_plan(&mut select_plan, schema)?;
let Plan::Select(ref select) = select_plan else {
let Plan::Select(ref select_ref) = select_plan else {
panic!("select_plan is not a SelectPlan");
};

let mut program = ProgramBuilder::new(ProgramBuilderOpts {
query_mode,
num_cursors: count_plan_required_cursors(select),
approx_num_insns: estimate_num_instructions(select),
approx_num_labels: estimate_num_labels(select),
num_cursors: count_plan_required_cursors(select_ref),
approx_num_insns: estimate_num_instructions(select_ref),
approx_num_labels: estimate_num_labels(select_ref),
});

if let ast::OneSelect::Values(values) = &select.body.select.as_ref() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we shouldn't need separate specialcasing to handle this. What about e.g. SELECT * from sometable JOIN (VALUES (1),(2),(3))?

It seems VALUES with multiple rows is quite similar to how subqueries work.

program.alloc_registers(values[0].len() + 1);
emit_values(&mut program, &values)?;
return Ok(program);
}

emit_program(&mut program, select_plan, syms)?;
Ok(program)
}
Expand Down Expand Up @@ -375,7 +383,43 @@ pub fn prepare_select_plan<'a>(
// Return the unoptimized query plan
Ok(Plan::Select(plan))
}
_ => todo!(),
ast::OneSelect::Values(values) => {
// New VALUES handling
if values.is_empty() {
crate::bail_parse_error!("VALUES clause cannot be empty");
}

let first_row_len = values[0].len();
if first_row_len == 0 {
crate::bail_parse_error!("VALUES rows must have at least one column");
}

// Create result columns from first row
let mut result_columns = Vec::with_capacity(first_row_len);
for i in 0..first_row_len {
result_columns.push(ResultSetColumn {
expr: values[0][i].clone(),
alias: None,
contains_aggregates: false,
});
}

// Create minimal plan since VALUES just returns constant rows
let plan = SelectPlan {
table_references: vec![], // No tables needed
result_columns,
where_clause: vec![],
group_by: None,
order_by: None,
aggregates: vec![],
limit: None,
offset: None,
contains_constant_false_condition: false,
query_type: SelectQueryType::TopLevel,
};

Ok(Plan::Select(plan))
}
}
}

Expand Down
61 changes: 61 additions & 0 deletions core/translate/values.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use crate::{
translate::emitter::Resolver,
translate::expr::translate_expr,
vdbe::{builder::ProgramBuilder, insn::Insn, BranchOffset},
LimboError, Result, SymbolTable,
};
use limbo_sqlite3_parser::ast;

pub fn emit_values(program: &mut ProgramBuilder, values: &[Vec<ast::Expr>]) -> Result<()> {
let goto_target = program.allocate_label();
program.emit_insn(Insn::Init {
target_pc: goto_target,
});

let start_offset = program.offset();

let start_reg = 1;
let first_row_len = values[0].len();
let num_regs = start_reg + first_row_len;

for _ in 0..num_regs {
program.alloc_register();
}

let symbol_table = SymbolTable::new();
let resolver = Resolver::new(&symbol_table);

for row in values {
if row.len() != first_row_len {
return Err(LimboError::ParseError(
"all VALUES rows must have the same number of values".into(),
));
}

for (i, expr) in row.iter().enumerate() {
let reg = start_reg + i;
translate_expr(program, None, expr, reg, &resolver)?;
}

program.emit_insn(Insn::ResultRow {
start_reg,
count: first_row_len,
});
}

program.emit_insn(Insn::Halt {
err_code: 0,
description: String::new(),
});

program.resolve_label(goto_target, program.offset());
program.emit_insn(Insn::Transaction { write: false });

program.emit_constant_insns();

program.emit_insn(Insn::Goto {
target_pc: start_offset,
});

Ok(())
}
144 changes: 144 additions & 0 deletions tests/integration/fuzz/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -913,4 +913,148 @@ mod tests {
);
}
}

#[test]
pub fn values_expression_fuzz_run() {
let _ = env_logger::try_init();
let g = GrammarGenerator::new();

let (expr, expr_builder) = g.create_handle();
let (values_row, values_row_builder) = g.create_handle();
let (values_list, values_list_builder) = g.create_handle();
let (literal, literal_builder) = g.create_handle();
let (number, number_builder) = g.create_handle();
let (string, string_builder) = g.create_handle();

number_builder
.choice()
.option_symbol(rand_int(-5..10))
.option_symbol(rand_int(-1000..1000))
.option_symbol(rand_int(-100000..100000))
.option_str("NULL")
.build();

string_builder
.concat("")
.push_str("'") // Open quote
.push_symbol(rand_str("abc123", 10))
.push_str("'") // Close quote
.build();

literal_builder
.choice()
.option(number)
.option(string)
.option_str("NULL")
.option_str("1.5")
.option_str("-2.5")
.option_str("0.0")
.build();

let fixed_columns = g
.create()
.concat("")
.push(literal)
.push_str(", ")
.push(literal)
.push_str(", ")
.push(literal)
.build();

values_row_builder
.concat("")
.push_str("(")
.push(fixed_columns)
.push_str(")")
.build();

values_list_builder
.concat("")
.push(
g.create()
.concat("")
.push(values_row)
.repeat(1..10, ", ")
.build(),
)
.build();

expr_builder
.concat("")
.push_str("VALUES ")
.push(values_list)
.build();

let db = TempDatabase::new_empty();
let limbo_conn = db.connect_limbo();
let sqlite_conn = rusqlite::Connection::open_in_memory().unwrap();

let (mut rng, seed) = rng_from_time();
log::info!("seed: {}", seed);

for i in 0..1024 {
let query = g.generate(&mut rng, expr, 50);
log::info!("Query {} of 0..1024: {}", i, query);

let limbo = limbo_exec_rows(&db, &limbo_conn, &query);
let sqlite = sqlite_exec_rows(&sqlite_conn, &query);

assert_eq!(
limbo, sqlite,
"Query: {}\nLimbo result: {:?}\nSQLite result: {:?}",
query, limbo, sqlite
);
}
}

#[test]
pub fn values_statement_edge_cases() {
let db = TempDatabase::new_empty();
let limbo_conn = db.connect_limbo();
let sqlite_conn = rusqlite::Connection::open_in_memory().unwrap();

// Test edge cases and specific scenarios
let test_cases = vec![
"VALUES (NULL)",
"VALUES (1), (2), (3)",
"VALUES (1, 2, 3)",
"VALUES ('hello', 1, NULL)",
"VALUES (1.5, -2.5, 0.0)",
"VALUES (9223372036854775807)",
"VALUES (-9223372036854775808)",
"VALUES (1, 'text with spaces', NULL)",
"VALUES (''), ('a'), ('ab')",
"VALUES (NULL, NULL), (1, 2)",
"VALUES (1), (2), (NULL), (4)",
"VALUES (1.0, 2.0), (3.0, 4.0)",
"VALUES (-1, -2), (-3, -4)",
"VALUES (1), (NULL), ('text')",
"VALUES (0.0), (-0.0), (1.0/0.0)",
"VALUES ('text''with''quotes')",
"VALUES ('Monty Python and the Holy Grail', 1975, 8.2)",
"VALUES ('And Now for Something Completely Different', 1971, 7.5)",
// Unary ops tests
"VALUES (-1)",
"VALUES (+1)",
"VALUES (+9223372036854775807)",
"VALUES (-1.7976931348623157e308)",
"VALUES (+1.7976931348623157e308)",
"VALUES (-0)",
"VALUES (+0)",
"VALUES (-NULL)",
"VALUES (-(1))",
];

for query in test_cases {
log::info!("Testing query: {}", query);
let limbo = limbo_exec_rows(&db, &limbo_conn, query);
let sqlite = sqlite_exec_rows(&sqlite_conn, query);

assert_eq!(
limbo, sqlite,
"query: {}, limbo: {:?}, sqlite: {:?}",
query, limbo, sqlite
);
}
}
}
Loading