Add and improve sqlite describe performance benchmarks (#2467)

* add basic describe benchmarks

* separate memory from query state

* move branch tracking & deduplication logic into a dedicated BranchList class

* Convert to using IntMap

* move intmap to a separate module, drop dead code, clean up function names

* Update Cargo.lock

* skip branches to check foreign keys, as they generally shouldn't impact query result type
This commit is contained in:
tyrelr 2023-05-08 13:44:28 -06:00 committed by GitHub
parent 1227d5d168
commit 0dfebb202f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 610 additions and 241 deletions

147
Cargo.lock generated
View file

@ -31,6 +31,12 @@ dependencies = [
"libc",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anyhow"
version = "1.0.58"
@ -409,6 +415,12 @@ dependencies = [
"serde_json",
]
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.0.73"
@ -433,6 +445,33 @@ dependencies = [
"winapi",
]
[[package]]
name = "ciborium"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369"
[[package]]
name = "ciborium-ll"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clap"
version = "3.2.10"
@ -572,6 +611,44 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff"
[[package]]
name = "criterion"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb"
dependencies = [
"anes",
"atty",
"cast",
"ciborium",
"clap",
"criterion-plot",
"futures",
"itertools",
"lazy_static",
"num-traits",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"tokio",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.5"
@ -1060,6 +1137,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "half"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
[[package]]
name = "hashbrown"
version = "0.12.2"
@ -1487,6 +1570,12 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
[[package]]
name = "oorandom"
version = "11.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]]
name = "openssl"
version = "0.10.41"
@ -1658,6 +1747,34 @@ version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
[[package]]
name = "plotters"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142"
[[package]]
name = "plotters-svg"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f"
dependencies = [
"plotters-backend",
]
[[package]]
name = "polling"
version = "2.2.0"
@ -2077,6 +2194,15 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.20"
@ -2283,6 +2409,7 @@ version = "0.7.0-alpha.2"
dependencies = [
"anyhow",
"async-std",
"criterion",
"dotenvy",
"env_logger",
"futures",
@ -2693,6 +2820,16 @@ dependencies = [
"time-core",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
@ -2911,6 +3048,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
name = "walkdir"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"

View file

@ -175,6 +175,8 @@ rand = "0.8.4"
rand_xoshiro = "0.6.0"
hex = "0.4.3"
tempdir = "0.3.7"
criterion = {version = "0.4", features = ["async_tokio"]}
# Needed to test SQLCipher
libsqlite3-sys = { version = "0.25.1", features = ["bundled-sqlcipher"] }
@ -250,6 +252,12 @@ name = "sqlite-migrate"
path = "tests/sqlite/migrate.rs"
required-features = ["sqlite", "macros", "migrate"]
[[bench]]
name = "sqlite-describe"
path = "benches/sqlite/describe.rs"
harness = false
required-features = ["sqlite"]
#
# MySQL
#
@ -332,3 +340,4 @@ required-features = ["postgres", "macros", "migrate"]
name = "postgres-migrate"
path = "tests/postgres/migrate.rs"
required-features = ["postgres", "macros", "migrate"]

141
benches/sqlite/describe.rs Normal file
View file

@ -0,0 +1,141 @@
use criterion::BenchmarkId;
use criterion::Criterion;
use criterion::{criterion_group, criterion_main};
use sqlx::sqlite::{Sqlite, SqliteConnection};
use sqlx::{Connection, Executor};
use sqlx_test::new;
// Here we have an async function to benchmark
async fn do_describe_trivial(db: &std::cell::RefCell<SqliteConnection>) {
db.borrow_mut().describe("select 1").await.unwrap();
}
async fn do_describe_recursive(db: &std::cell::RefCell<SqliteConnection>) {
db.borrow_mut()
.describe(
r#"
WITH RECURSIVE schedule(begin_date) AS MATERIALIZED (
SELECT datetime('2022-10-01')
WHERE datetime('2022-10-01') < datetime('2022-11-03')
UNION ALL
SELECT datetime(begin_date,'+1 day')
FROM schedule
WHERE datetime(begin_date) < datetime(?2)
)
SELECT
begin_date
FROM schedule
GROUP BY begin_date
"#,
)
.await
.unwrap();
}
async fn do_describe_insert(db: &std::cell::RefCell<SqliteConnection>) {
db.borrow_mut()
.describe("INSERT INTO tweet (id, text) VALUES (2, 'Hello') RETURNING *")
.await
.unwrap();
}
async fn do_describe_insert_fks(db: &std::cell::RefCell<SqliteConnection>) {
db.borrow_mut()
.describe("insert into statements (text) values ('a') returning id")
.await
.unwrap();
}
async fn init_connection() -> SqliteConnection {
let mut conn = new::<Sqlite>().await.unwrap();
conn.execute(
r#"
CREATE TEMPORARY TABLE statements (
id integer not null primary key,
text text not null
);
CREATE TEMPORARY TABLE votes1 (statement_id integer not null references statements(id));
CREATE TEMPORARY TABLE votes2 (statement_id integer not null references statements(id));
CREATE TEMPORARY TABLE votes3 (statement_id integer not null references statements(id));
CREATE TEMPORARY TABLE votes4 (statement_id integer not null references statements(id));
CREATE TEMPORARY TABLE votes5 (statement_id integer not null references statements(id));
CREATE TEMPORARY TABLE votes6 (statement_id integer not null references statements(id));
--CREATE TEMPORARY TABLE votes7 (statement_id integer not null references statements(id));
--CREATE TEMPORARY TABLE votes8 (statement_id integer not null references statements(id));
--CREATE TEMPORARY TABLE votes9 (statement_id integer not null references statements(id));
--CREATE TEMPORARY TABLE votes10 (statement_id integer not null references statements(id));
--CREATE TEMPORARY TABLE votes11 (statement_id integer not null references statements(id));
"#,
)
.await
.unwrap();
conn
}
fn describe_trivial(c: &mut Criterion) {
let runtime = tokio::runtime::Runtime::new().unwrap();
let db = std::cell::RefCell::new(runtime.block_on(init_connection()));
c.bench_with_input(
BenchmarkId::new("select", "trivial"),
&db,
move |b, db_ref| {
// Insert a call to `to_async` to convert the bencher to async mode.
// The timing loops are the same as with the normal bencher.
b.to_async(&runtime).iter(|| do_describe_trivial(db_ref));
},
);
}
fn describe_recursive(c: &mut Criterion) {
let runtime = tokio::runtime::Runtime::new().unwrap();
let db = std::cell::RefCell::new(runtime.block_on(init_connection()));
c.bench_with_input(
BenchmarkId::new("select", "recursive"),
&db,
move |b, db_ref| {
// Insert a call to `to_async` to convert the bencher to async mode.
// The timing loops are the same as with the normal bencher.
b.to_async(&runtime).iter(|| do_describe_recursive(db_ref));
},
);
}
fn describe_insert(c: &mut Criterion) {
let runtime = tokio::runtime::Runtime::new().unwrap();
let db = std::cell::RefCell::new(runtime.block_on(init_connection()));
c.bench_with_input(
BenchmarkId::new("insert", "returning"),
&db,
move |b, db_ref| {
// Insert a call to `to_async` to convert the bencher to async mode.
// The timing loops are the same as with the normal bencher.
b.to_async(&runtime).iter(|| do_describe_insert(db_ref));
},
);
}
fn describe_insert_fks(c: &mut Criterion) {
let runtime = tokio::runtime::Runtime::new().unwrap();
let db = std::cell::RefCell::new(runtime.block_on(init_connection()));
c.bench_with_input(BenchmarkId::new("insert", "fks"), &db, move |b, db_ref| {
// Insert a call to `to_async` to convert the bencher to async mode.
// The timing loops are the same as with the normal bencher.
b.to_async(&runtime).iter(|| do_describe_insert_fks(db_ref));
});
}
criterion_group!(
benches,
describe_trivial,
describe_recursive,
describe_insert,
describe_insert_fks
);
criterion_main!(benches);

View file

@ -1,3 +1,4 @@
use crate::connection::intmap::IntMap;
use crate::connection::{execute, ConnectionState};
use crate::error::Error;
use crate::from_row::FromRow;
@ -136,7 +137,7 @@ enum ColumnType {
datatype: DataType,
nullable: Option<bool>,
},
Record(Vec<ColumnType>),
Record(IntMap<ColumnType>),
}
impl Default for ColumnType {
@ -199,60 +200,37 @@ impl RegDataType {
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
enum CursorDataType {
Normal {
cols: HashMap<i64, ColumnType>,
cols: IntMap<ColumnType>,
is_empty: Option<bool>,
},
Pseudo(i64),
}
impl CursorDataType {
fn from_sparse_record(record: &HashMap<i64, ColumnType>, is_empty: Option<bool>) -> Self {
fn from_intmap(record: &IntMap<ColumnType>, is_empty: Option<bool>) -> Self {
Self::Normal {
cols: record
.iter()
.map(|(colnum, datatype)| (*colnum, datatype.clone()))
.collect(),
cols: record.clone(),
is_empty,
}
}
fn from_dense_record(record: &Vec<ColumnType>, is_empty: Option<bool>) -> Self {
Self::Normal {
cols: (0..).zip(record.iter().cloned()).collect(),
cols: IntMap::from_dense_record(record),
is_empty,
}
}
fn map_to_dense_record(&self, registers: &HashMap<i64, RegDataType>) -> Vec<ColumnType> {
match self {
Self::Normal { cols, .. } => {
let mut rowdata = vec![ColumnType::default(); cols.len()];
for (idx, col) in cols.iter() {
rowdata[*idx as usize] = col.clone();
}
rowdata
}
Self::Pseudo(i) => match registers.get(i) {
Some(RegDataType::Single(ColumnType::Record(r))) => r.clone(),
_ => Vec::new(),
},
}
}
fn map_to_sparse_record(
&self,
registers: &HashMap<i64, RegDataType>,
) -> HashMap<i64, ColumnType> {
fn map_to_intmap(&self, registers: &IntMap<RegDataType>) -> IntMap<ColumnType> {
match self {
Self::Normal { cols, .. } => cols.clone(),
Self::Pseudo(i) => match registers.get(i) {
Some(RegDataType::Single(ColumnType::Record(r))) => {
(0..).zip(r.iter().cloned()).collect()
}
_ => HashMap::new(),
Some(RegDataType::Single(ColumnType::Record(r))) => r.clone(),
_ => IntMap::new(),
},
}
}
@ -292,7 +270,7 @@ fn opcode_to_type(op: &str) -> DataType {
fn root_block_columns(
conn: &mut ConnectionState,
) -> Result<HashMap<(i64, i64), HashMap<i64, ColumnType>>, Error> {
) -> Result<HashMap<(i64, i64), IntMap<ColumnType>>, Error> {
let table_block_columns: Vec<(i64, i64, i64, String, bool)> = execute::iter(
conn,
"SELECT s.dbnum, s.rootpage, col.cid as colnum, col.type, col.\"notnull\"
@ -319,7 +297,7 @@ fn root_block_columns(
.map(|row| FromRow::from_row(&row?))
.collect::<Result<Vec<_>, Error>>()?;
let mut row_info: HashMap<(i64, i64), HashMap<i64, ColumnType>> = HashMap::new();
let mut row_info: HashMap<(i64, i64), IntMap<ColumnType>> = HashMap::new();
for (dbnum, block, colnum, datatype, notnull) in table_block_columns {
let row_info = row_info.entry((dbnum, block)).or_default();
row_info.insert(
@ -340,66 +318,43 @@ struct QueryState {
pub visited: Vec<u8>,
// A log of the order of execution of each instruction
pub history: Vec<usize>,
// Registers
pub r: HashMap<i64, RegDataType>,
// Rows that pointers point to
pub p: HashMap<i64, CursorDataType>,
// Next instruction to execute
pub program_i: usize,
// State of the virtual machine
pub mem: MemoryState,
// Results published by the execution
pub result: Option<Vec<(Option<SqliteTypeInfo>, Option<bool>)>>,
}
#[derive(Debug, Hash, PartialEq, Eq)]
struct BranchStateHash {
instruction: usize,
//register index, data type
registers: Vec<(i64, RegDataType)>,
//cursor index, is_empty, pseudo register index
cursor_metadata: Vec<(i64, Option<bool>, Option<i64>)>,
//cursor index, column index, data type
cursors: Vec<(i64, i64, Option<ColumnType>)>,
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct MemoryState {
// Next instruction to execute
pub program_i: usize,
// Registers
pub r: IntMap<RegDataType>,
// Rows that pointers point to
pub p: IntMap<CursorDataType>,
}
impl BranchStateHash {
pub fn from_query_state(st: &QueryState) -> Self {
let mut reg = vec![];
for (k, v) in &st.r {
reg.push((*k, v.clone()));
}
reg.sort_by_key(|v| v.0);
struct BranchList {
states: Vec<QueryState>,
visited_branch_state: HashSet<MemoryState>,
}
let mut cur = vec![];
let mut cur_meta = vec![];
for (k, v) in &st.p {
match v {
CursorDataType::Normal { cols, is_empty } => {
cur_meta.push((*k, *is_empty, None));
for (i, col) in cols {
cur.push((*k, *i, Some(col.clone())));
}
}
CursorDataType::Pseudo(i) => {
cur_meta.push((*k, None, Some(*i)));
//don't bother copying columns, they are in register i
}
}
}
cur_meta.sort_by(|a, b| a.0.cmp(&b.0));
cur.sort_by(|a, b| {
if a.0 == b.0 {
a.1.cmp(&b.1)
} else {
a.0.cmp(&b.0)
}
});
impl BranchList {
pub fn new(state: QueryState) -> Self {
Self {
instruction: st.program_i,
registers: reg,
cursor_metadata: cur_meta,
cursors: cur,
states: vec![state],
visited_branch_state: HashSet::new(),
}
}
pub fn push(&mut self, state: QueryState) {
if !self.visited_branch_state.contains(&state.mem) {
self.visited_branch_state.insert(state.mem.clone());
self.states.push(state);
}
}
pub fn pop(&mut self) -> Option<QueryState> {
self.states.pop()
}
}
// Opcode Reference: https://sqlite.org/opcode.html
@ -418,24 +373,24 @@ pub(super) fn explain(
let mut logger =
crate::logger::QueryPlanLogger::new(query, &program, conn.log_settings.clone());
let mut states = vec![QueryState {
let mut states = BranchList::new(QueryState {
visited: vec![0; program_size],
history: Vec::new(),
r: HashMap::with_capacity(6),
p: HashMap::with_capacity(6),
program_i: 0,
result: None,
}];
let mut visited_branch_state: HashSet<BranchStateHash> = HashSet::new();
mem: MemoryState {
program_i: 0,
r: IntMap::new(),
p: IntMap::new(),
},
});
let mut gas = MAX_TOTAL_INSTRUCTION_COUNT;
let mut result_states = Vec::new();
while let Some(mut state) = states.pop() {
while state.program_i < program_size {
let (_, ref opcode, p1, p2, p3, ref p4) = program[state.program_i];
state.history.push(state.program_i);
while state.mem.program_i < program_size {
let (_, ref opcode, p1, p2, p3, ref p4) = program[state.mem.program_i];
state.history.push(state.mem.program_i);
//limit the number of 'instructions' that can be evaluated
if gas > 0 {
@ -444,7 +399,7 @@ pub(super) fn explain(
break;
}
if state.visited[state.program_i] > MAX_LOOP_COUNT {
if state.visited[state.mem.program_i] > MAX_LOOP_COUNT {
if logger.log_enabled() {
let program_history: Vec<&(i64, String, i64, i64, i64, Vec<u8>)> =
state.history.iter().map(|i| &program[*i]).collect();
@ -455,32 +410,42 @@ pub(super) fn explain(
break;
}
state.visited[state.program_i] += 1;
state.visited[state.mem.program_i] += 1;
match &**opcode {
OP_INIT => {
// start at <p2>
state.program_i = p2 as usize;
state.mem.program_i = p2 as usize;
continue;
}
OP_GOTO => {
// goto <p2>
state.program_i = p2 as usize;
state.mem.program_i = p2 as usize;
continue;
}
OP_GO_SUB => {
// store current instruction in r[p1], goto <p2>
state.r.insert(p1, RegDataType::Int(state.program_i as i64));
state.program_i = p2 as usize;
state
.mem
.r
.insert(p1, RegDataType::Int(state.mem.program_i as i64));
state.mem.program_i = p2 as usize;
continue;
}
OP_DECR_JUMP_ZERO | OP_ELSE_EQ | OP_EQ | OP_FILTER | OP_FK_IF_ZERO | OP_FOUND
| OP_GE | OP_GT | OP_IDX_GE | OP_IDX_GT | OP_IDX_LE | OP_IDX_LT | OP_IF_NO_HOPE
| OP_IF_NOT | OP_IF_NOT_OPEN | OP_IF_NOT_ZERO | OP_IF_NULL_ROW | OP_IF_SMALLER
OP_FK_IF_ZERO => {
// goto <p2> if no constraints are unsatisfied (assumed to be true)
state.mem.program_i = p2 as usize;
continue;
}
OP_DECR_JUMP_ZERO | OP_ELSE_EQ | OP_EQ | OP_FILTER | OP_FOUND | OP_GE | OP_GT
| OP_IDX_GE | OP_IDX_GT | OP_IDX_LE | OP_IDX_LT | OP_IF_NO_HOPE | OP_IF_NOT
| OP_IF_NOT_OPEN | OP_IF_NOT_ZERO | OP_IF_NULL_ROW | OP_IF_SMALLER
| OP_INCR_VACUUM | OP_IS_NULL | OP_IS_NULL_OR_TYPE | OP_LE | OP_LT | OP_NE
| OP_NEXT | OP_NO_CONFLICT | OP_NOT_EXISTS | OP_ONCE | OP_PREV | OP_PROGRAM
| OP_ROW_SET_READ | OP_ROW_SET_TEST | OP_SEEK_GE | OP_SEEK_GT | OP_SEEK_LE
@ -489,50 +454,42 @@ pub(super) fn explain(
// goto <p2> or next instruction (depending on actual values)
let mut branch_state = state.clone();
branch_state.program_i = p2 as usize;
let bs_hash = BranchStateHash::from_query_state(&branch_state);
if !visited_branch_state.contains(&bs_hash) {
visited_branch_state.insert(bs_hash);
branch_state.mem.program_i = p2 as usize;
states.push(branch_state);
}
state.program_i += 1;
state.mem.program_i += 1;
continue;
}
OP_NOT_NULL => {
// goto <p2> or next instruction (depending on actual values)
let might_branch = match state.r.get(&p1) {
let might_branch = match state.mem.r.get(&p1) {
Some(r_p1) => !matches!(r_p1.map_to_datatype(), DataType::Null),
_ => false,
};
let might_not_branch = match state.r.get(&p1) {
let might_not_branch = match state.mem.r.get(&p1) {
Some(r_p1) => !matches!(r_p1.map_to_nullable(), Some(false)),
_ => false,
};
if might_branch {
let mut branch_state = state.clone();
branch_state.program_i = p2 as usize;
branch_state.mem.program_i = p2 as usize;
if let Some(RegDataType::Single(ColumnType::Single { nullable, .. })) =
branch_state.r.get_mut(&p1)
branch_state.mem.r.get_mut(&p1)
{
*nullable = Some(false);
}
let bs_hash = BranchStateHash::from_query_state(&branch_state);
if !visited_branch_state.contains(&bs_hash) {
visited_branch_state.insert(bs_hash);
states.push(branch_state);
}
}
if might_not_branch {
state.program_i += 1;
state.mem.program_i += 1;
state
.mem
.r
.insert(p1, RegDataType::Single(ColumnType::default()));
continue;
@ -548,50 +505,41 @@ pub(super) fn explain(
//don't bother checking actual types, just don't branch to instruction 0
if p2 != 0 {
let mut branch_state = state.clone();
branch_state.program_i = p2 as usize;
let bs_hash = BranchStateHash::from_query_state(&branch_state);
if !visited_branch_state.contains(&bs_hash) {
visited_branch_state.insert(bs_hash);
branch_state.mem.program_i = p2 as usize;
states.push(branch_state);
}
}
state.program_i += 1;
state.mem.program_i += 1;
continue;
}
OP_IF => {
// goto <p2> if r[p1] is true (1) or r[p1] is null and p3 is nonzero
let might_branch = match state.r.get(&p1) {
let might_branch = match state.mem.r.get(&p1) {
Some(RegDataType::Int(r_p1)) => *r_p1 != 0,
_ => true,
};
let might_not_branch = match state.r.get(&p1) {
let might_not_branch = match state.mem.r.get(&p1) {
Some(RegDataType::Int(r_p1)) => *r_p1 == 0,
_ => true,
};
if might_branch {
let mut branch_state = state.clone();
branch_state.program_i = p2 as usize;
branch_state.mem.program_i = p2 as usize;
if p3 == 0 {
branch_state.r.insert(p1, RegDataType::Int(1));
branch_state.mem.r.insert(p1, RegDataType::Int(1));
}
let bs_hash = BranchStateHash::from_query_state(&branch_state);
if !visited_branch_state.contains(&bs_hash) {
visited_branch_state.insert(bs_hash);
states.push(branch_state);
}
}
if might_not_branch {
state.program_i += 1;
state.mem.program_i += 1;
if p3 == 0 {
state.r.insert(p1, RegDataType::Int(0));
state.mem.r.insert(p1, RegDataType::Int(0));
}
continue;
} else {
@ -604,34 +552,34 @@ pub(super) fn explain(
// as a workaround for large offset clauses, both branches will be attempted after 1 loop
let might_branch = match state.r.get(&p1) {
let might_branch = match state.mem.r.get(&p1) {
Some(RegDataType::Int(r_p1)) => *r_p1 >= 1,
_ => true,
};
let might_not_branch = match state.r.get(&p1) {
let might_not_branch = match state.mem.r.get(&p1) {
Some(RegDataType::Int(r_p1)) => *r_p1 < 1,
_ => true,
};
let loop_detected = state.visited[state.program_i] > 1;
let loop_detected = state.visited[state.mem.program_i] > 1;
if might_branch || loop_detected {
let mut branch_state = state.clone();
branch_state.program_i = p2 as usize;
if let Some(RegDataType::Int(r_p1)) = branch_state.r.get_mut(&p1) {
branch_state.mem.program_i = p2 as usize;
if let Some(RegDataType::Int(r_p1)) = branch_state.mem.r.get_mut(&p1) {
*r_p1 -= 1;
}
states.push(branch_state);
}
if might_not_branch {
state.program_i += 1;
state.mem.program_i += 1;
continue;
} else if loop_detected {
state.program_i += 1;
if matches!(state.r.get_mut(&p1), Some(RegDataType::Int(..))) {
state.mem.program_i += 1;
if matches!(state.mem.r.get_mut(&p1), Some(RegDataType::Int(..))) {
//forget the exact value, in case some later cares
state.r.insert(
state.mem.r.insert(
p1,
RegDataType::Single(ColumnType::Single {
datatype: DataType::Int64,
@ -649,19 +597,19 @@ pub(super) fn explain(
// goto <p2> if cursor p1 is empty and p2 != 0, else next instruction
if p2 == 0 {
state.program_i += 1;
state.mem.program_i += 1;
continue;
}
if let Some(cursor) = state.p.get(&p1) {
if let Some(cursor) = state.mem.p.get(&p1) {
if matches!(cursor.is_empty(), None | Some(true)) {
//only take this branch if the cursor is empty
let mut branch_state = state.clone();
branch_state.program_i = p2 as usize;
branch_state.mem.program_i = p2 as usize;
if let Some(CursorDataType::Normal { is_empty, .. }) =
branch_state.p.get_mut(&p1)
branch_state.mem.p.get_mut(&p1)
{
*is_empty = Some(true);
}
@ -670,7 +618,7 @@ pub(super) fn explain(
if matches!(cursor.is_empty(), None | Some(false)) {
//only take this branch if the cursor is non-empty
state.program_i += 1;
state.mem.program_i += 1;
continue;
} else {
break;
@ -689,12 +637,12 @@ pub(super) fn explain(
OP_INIT_COROUTINE => {
// goto <p2> or next instruction (depending on actual values)
state.r.insert(p1, RegDataType::Int(p3));
state.mem.r.insert(p1, RegDataType::Int(p3));
if p2 != 0 {
state.program_i = p2 as usize;
state.mem.program_i = p2 as usize;
} else {
state.program_i += 1;
state.mem.program_i += 1;
}
continue;
}
@ -702,13 +650,13 @@ pub(super) fn explain(
OP_END_COROUTINE => {
// jump to p2 of the yield instruction pointed at by register p1
if let Some(RegDataType::Int(yield_i)) = state.r.get(&p1) {
if let Some(RegDataType::Int(yield_i)) = state.mem.r.get(&p1) {
if let Some((_, yield_op, _, yield_p2, _, _)) =
program.get(*yield_i as usize)
{
if OP_YIELD == yield_op.as_str() {
state.program_i = (*yield_p2) as usize;
state.r.remove(&p1);
state.mem.program_i = (*yield_p2) as usize;
state.mem.r.remove(&p1);
continue;
} else {
if logger.log_enabled() {
@ -746,9 +694,9 @@ pub(super) fn explain(
OP_RETURN => {
// jump to the instruction after the instruction pointed at by register p1
if let Some(RegDataType::Int(return_i)) = state.r.get(&p1) {
state.program_i = (*return_i + 1) as usize;
state.r.remove(&p1);
if let Some(RegDataType::Int(return_i)) = state.mem.r.get(&p1) {
state.mem.program_i = (*return_i + 1) as usize;
state.mem.r.remove(&p1);
continue;
} else {
if logger.log_enabled() {
@ -763,8 +711,8 @@ pub(super) fn explain(
OP_YIELD => {
// jump to p2 of the yield instruction pointed at by register p1, store prior instruction in p1
if let Some(RegDataType::Int(yield_i)) = state.r.get_mut(&p1) {
let program_i: usize = state.program_i;
if let Some(RegDataType::Int(yield_i)) = state.mem.r.get_mut(&p1) {
let program_i: usize = state.mem.program_i;
//if yielding to a yield operation, go to the NEXT instruction after that instruction
if program
@ -772,11 +720,11 @@ pub(super) fn explain(
.map(|(_, yield_op, _, _, _, _)| yield_op.as_str())
== Some(OP_YIELD)
{
state.program_i = (*yield_i + 1) as usize;
state.mem.program_i = (*yield_i + 1) as usize;
*yield_i = program_i as i64;
continue;
} else {
state.program_i = *yield_i as usize;
state.mem.program_i = *yield_i as usize;
*yield_i = program_i as i64;
continue;
}
@ -794,44 +742,35 @@ pub(super) fn explain(
// goto one of <p1>, <p2>, or <p3> based on the result of a prior compare
let mut branch_state = state.clone();
branch_state.program_i = p1 as usize;
let bs_hash = BranchStateHash::from_query_state(&branch_state);
if !visited_branch_state.contains(&bs_hash) {
visited_branch_state.insert(bs_hash);
branch_state.mem.program_i = p1 as usize;
states.push(branch_state);
}
let mut branch_state = state.clone();
branch_state.program_i = p2 as usize;
let bs_hash = BranchStateHash::from_query_state(&branch_state);
if !visited_branch_state.contains(&bs_hash) {
visited_branch_state.insert(bs_hash);
branch_state.mem.program_i = p2 as usize;
states.push(branch_state);
}
let mut branch_state = state.clone();
branch_state.program_i = p3 as usize;
let bs_hash = BranchStateHash::from_query_state(&branch_state);
if !visited_branch_state.contains(&bs_hash) {
visited_branch_state.insert(bs_hash);
branch_state.mem.program_i = p3 as usize;
states.push(branch_state);
}
}
OP_COLUMN => {
//Get the row stored at p1, or NULL; get the column stored at p2, or NULL
if let Some(record) = state.p.get(&p1).map(|c| c.map_to_sparse_record(&state.r))
if let Some(record) =
state.mem.p.get(&p1).map(|c| c.map_to_intmap(&state.mem.r))
{
if let Some(col) = record.get(&p2) {
// insert into p3 the datatype of the col
state.r.insert(p3, RegDataType::Single(col.clone()));
state.mem.r.insert(p3, RegDataType::Single(col.clone()));
} else {
state
.mem
.r
.insert(p3, RegDataType::Single(ColumnType::default()));
}
} else {
state
.mem
.r
.insert(p3, RegDataType::Single(ColumnType::default()));
}
@ -841,7 +780,7 @@ pub(super) fn explain(
//Copy sequence number from cursor p1 to register p2, increment cursor p1 sequence number
//Cursor emulation doesn't sequence value, but it is an int
state.r.insert(
state.mem.r.insert(
p2,
RegDataType::Single(ColumnType::Single {
datatype: DataType::Int64,
@ -852,15 +791,17 @@ pub(super) fn explain(
OP_ROW_DATA | OP_SORTER_DATA => {
//Get entire row from cursor p1, store it into register p2
if let Some(record) = state.p.get(&p1) {
let rowdata = record.map_to_dense_record(&state.r);
if let Some(record) = state.mem.p.get(&p1) {
let rowdata = record.map_to_intmap(&state.mem.r);
state
.mem
.r
.insert(p2, RegDataType::Single(ColumnType::Record(rowdata)));
} else {
state
.mem
.r
.insert(p2, RegDataType::Single(ColumnType::Record(Vec::new())));
.insert(p2, RegDataType::Single(ColumnType::Record(IntMap::new())));
}
}
@ -870,25 +811,28 @@ pub(super) fn explain(
for reg in p1..p1 + p2 {
record.push(
state
.mem
.r
.get(&reg)
.map(|d| d.clone().map_to_columntype())
.unwrap_or(ColumnType::default()),
);
}
state
.r
.insert(p3, RegDataType::Single(ColumnType::Record(record)));
state.mem.r.insert(
p3,
RegDataType::Single(ColumnType::Record(IntMap::from_dense_record(&record))),
);
}
OP_INSERT | OP_IDX_INSERT | OP_SORTER_INSERT => {
if let Some(RegDataType::Single(ColumnType::Record(record))) = state.r.get(&p2)
if let Some(RegDataType::Single(ColumnType::Record(record))) =
state.mem.r.get(&p2)
{
if let Some(CursorDataType::Normal { cols, is_empty }) =
state.p.get_mut(&p1)
state.mem.p.get_mut(&p1)
{
// Insert the record into wherever pointer p1 is
*cols = (0..).zip(record.iter().cloned()).collect();
*cols = record.clone();
*is_empty = Some(false);
}
}
@ -897,7 +841,8 @@ pub(super) fn explain(
OP_DELETE => {
// delete a record from cursor p1
if let Some(CursorDataType::Normal { is_empty, .. }) = state.p.get_mut(&p1) {
if let Some(CursorDataType::Normal { is_empty, .. }) = state.mem.p.get_mut(&p1)
{
if *is_empty == Some(false) {
*is_empty = None; //the cursor might be empty now
}
@ -906,7 +851,7 @@ pub(super) fn explain(
OP_OPEN_PSEUDO => {
// Create a cursor p1 aliasing the record from register p2
state.p.insert(p1, CursorDataType::Pseudo(p2));
state.mem.p.insert(p1, CursorDataType::Pseudo(p2));
}
OP_OPEN_READ | OP_OPEN_WRITE => {
@ -914,22 +859,23 @@ pub(super) fn explain(
if p3 == 0 || p3 == 1 {
if let Some(columns) = root_block_cols.get(&(p3, p2)) {
state
.mem
.p
.insert(p1, CursorDataType::from_sparse_record(columns, None));
.insert(p1, CursorDataType::from_intmap(columns, None));
} else {
state.p.insert(
state.mem.p.insert(
p1,
CursorDataType::Normal {
cols: HashMap::with_capacity(6),
cols: IntMap::new(),
is_empty: None,
},
);
}
} else {
state.p.insert(
state.mem.p.insert(
p1,
CursorDataType::Normal {
cols: HashMap::with_capacity(6),
cols: IntMap::new(),
is_empty: None,
},
);
@ -938,7 +884,7 @@ pub(super) fn explain(
OP_OPEN_EPHEMERAL | OP_OPEN_AUTOINDEX | OP_SORTER_OPEN => {
//Create a new pointer which is referenced by p1
state.p.insert(
state.mem.p.insert(
p1,
CursorDataType::from_dense_record(
&vec![ColumnType::null(); p2 as usize],
@ -949,14 +895,17 @@ pub(super) fn explain(
OP_VARIABLE => {
// r[p2] = <value of variable>
state.r.insert(p2, RegDataType::Single(ColumnType::null()));
state
.mem
.r
.insert(p2, RegDataType::Single(ColumnType::null()));
}
// if there is a value in p3, and the query passes, then
// we know that it is not nullable
OP_HALT_IF_NULL => {
if let Some(RegDataType::Single(ColumnType::Single { nullable, .. })) =
state.r.get_mut(&p3)
state.mem.r.get_mut(&p3)
{
*nullable = Some(false);
}
@ -967,7 +916,7 @@ pub(super) fn explain(
match from_utf8(p4).map_err(Error::protocol)? {
"last_insert_rowid(0)" => {
// last_insert_rowid() -> INTEGER
state.r.insert(
state.mem.r.insert(
p3,
RegDataType::Single(ColumnType::Single {
datatype: DataType::Int64,
@ -977,7 +926,7 @@ pub(super) fn explain(
}
"date(-1)" | "time(-1)" | "datetime(-1)" | "strftime(-1)" => {
// date|time|datetime|strftime(...) -> TEXT
state.r.insert(
state.mem.r.insert(
p3,
RegDataType::Single(ColumnType::Single {
datatype: DataType::Text,
@ -987,7 +936,7 @@ pub(super) fn explain(
}
"julianday(-1)" => {
// julianday(...) -> REAL
state.r.insert(
state.mem.r.insert(
p3,
RegDataType::Single(ColumnType::Single {
datatype: DataType::Float,
@ -997,7 +946,7 @@ pub(super) fn explain(
}
"unixepoch(-1)" => {
// unixepoch(p2...) -> INTEGER
state.r.insert(
state.mem.r.insert(
p3,
RegDataType::Single(ColumnType::Single {
datatype: DataType::Int64,
@ -1006,13 +955,14 @@ pub(super) fn explain(
);
}
_ => logger.add_unknown_operation(&program[state.program_i]),
_ => logger.add_unknown_operation(&program[state.mem.program_i]),
}
}
OP_NULL_ROW => {
// all columns in cursor X are potentially nullable
if let Some(CursorDataType::Normal { ref mut cols, .. }) = state.p.get_mut(&p1)
if let Some(CursorDataType::Normal { ref mut cols, .. }) =
state.mem.p.get_mut(&p1)
{
for col in cols.values_mut() {
if let ColumnType::Single {
@ -1037,7 +987,7 @@ pub(super) fn explain(
|| p4.starts_with("ntile(")
{
// count(_) -> INTEGER
state.r.insert(
state.mem.r.insert(
p3,
RegDataType::Single(ColumnType::Single {
datatype: DataType::Int64,
@ -1045,7 +995,7 @@ pub(super) fn explain(
}),
);
} else if p4.starts_with("sum(") {
if let Some(r_p2) = state.r.get(&p2) {
if let Some(r_p2) = state.mem.r.get(&p2) {
let datatype = match r_p2.map_to_datatype() {
DataType::Int64 => DataType::Int64,
DataType::Int => DataType::Int,
@ -1053,14 +1003,14 @@ pub(super) fn explain(
_ => DataType::Float,
};
let nullable = r_p2.map_to_nullable();
state.r.insert(
state.mem.r.insert(
p3,
RegDataType::Single(ColumnType::Single { datatype, nullable }),
);
}
} else if let Some(v) = state.r.get(&p2).cloned() {
} else if let Some(v) = state.mem.r.get(&p2).cloned() {
// r[p3] = AGG ( r[p2] )
state.r.insert(p3, v);
state.mem.r.insert(p3, v);
}
}
@ -1074,7 +1024,7 @@ pub(super) fn explain(
|| p4.starts_with("ntile(")
{
// count(_) -> INTEGER
state.r.insert(
state.mem.r.insert(
p1,
RegDataType::Single(ColumnType::Single {
datatype: DataType::Int64,
@ -1086,7 +1036,7 @@ pub(super) fn explain(
OP_CAST => {
// affinity(r[p1])
if let Some(v) = state.r.get_mut(&p1) {
if let Some(v) = state.mem.r.get_mut(&p1) {
*v = RegDataType::Single(ColumnType::Single {
datatype: affinity_to_type(p2 as u8),
nullable: v.map_to_nullable(),
@ -1096,8 +1046,8 @@ pub(super) fn explain(
OP_SCOPY | OP_INT_COPY => {
// r[p2] = r[p1]
if let Some(v) = state.r.get(&p1).cloned() {
state.r.insert(p2, v);
if let Some(v) = state.mem.r.get(&p1).cloned() {
state.mem.r.insert(p2, v);
}
}
@ -1107,8 +1057,8 @@ pub(super) fn explain(
for i in 0..=p3 {
let src = p1 + i;
let dst = p2 + i;
if let Some(v) = state.r.get(&src).cloned() {
state.r.insert(dst, v);
if let Some(v) = state.mem.r.get(&src).cloned() {
state.mem.r.insert(dst, v);
}
}
}
@ -1120,9 +1070,12 @@ pub(super) fn explain(
for i in 0..p3 {
let src = p1 + i;
let dst = p2 + i;
if let Some(v) = state.r.get(&src).cloned() {
state.r.insert(dst, v);
state.r.insert(src, RegDataType::Single(ColumnType::null()));
if let Some(v) = state.mem.r.get(&src).cloned() {
state.mem.r.insert(dst, v);
state
.mem
.r
.insert(src, RegDataType::Single(ColumnType::null()));
}
}
}
@ -1130,12 +1083,12 @@ pub(super) fn explain(
OP_INTEGER => {
// r[p2] = p1
state.r.insert(p2, RegDataType::Int(p1));
state.mem.r.insert(p2, RegDataType::Int(p1));
}
OP_BLOB | OP_COUNT | OP_REAL | OP_STRING8 | OP_ROWID | OP_NEWROWID => {
// r[p2] = <value of constant>
state.r.insert(
state.mem.r.insert(
p2,
RegDataType::Single(ColumnType::Single {
datatype: opcode_to_type(&opcode),
@ -1146,8 +1099,8 @@ pub(super) fn explain(
OP_NOT => {
// r[p2] = NOT r[p1]
if let Some(a) = state.r.get(&p1).cloned() {
state.r.insert(p2, a);
if let Some(a) = state.mem.r.get(&p1).cloned() {
state.mem.r.insert(p2, a);
}
}
@ -1156,16 +1109,19 @@ pub(super) fn explain(
let idx_range = if p2 < p3 { p2..=p3 } else { p2..=p2 };
for idx in idx_range {
state.r.insert(idx, RegDataType::Single(ColumnType::null()));
state
.mem
.r
.insert(idx, RegDataType::Single(ColumnType::null()));
}
}
OP_OR | OP_AND | OP_BIT_AND | OP_BIT_OR | OP_SHIFT_LEFT | OP_SHIFT_RIGHT
| OP_ADD | OP_SUBTRACT | OP_MULTIPLY | OP_DIVIDE | OP_REMAINDER | OP_CONCAT => {
// r[p3] = r[p1] + r[p2]
match (state.r.get(&p1).cloned(), state.r.get(&p2).cloned()) {
match (state.mem.r.get(&p1).cloned(), state.mem.r.get(&p2).cloned()) {
(Some(a), Some(b)) => {
state.r.insert(
state.mem.r.insert(
p3,
RegDataType::Single(ColumnType::Single {
datatype: if matches!(a.map_to_datatype(), DataType::Null) {
@ -1184,7 +1140,7 @@ pub(super) fn explain(
}
(Some(v), None) => {
state.r.insert(
state.mem.r.insert(
p3,
RegDataType::Single(ColumnType::Single {
datatype: v.map_to_datatype(),
@ -1194,7 +1150,7 @@ pub(super) fn explain(
}
(None, Some(v)) => {
state.r.insert(
state.mem.r.insert(
p3,
RegDataType::Single(ColumnType::Single {
datatype: v.map_to_datatype(),
@ -1209,7 +1165,7 @@ pub(super) fn explain(
OP_OFFSET_LIMIT => {
// r[p2] = if r[p2] < 0 { r[p1] } else if r[p1]<0 { -1 } else { r[p1] + r[p3] }
state.r.insert(
state.mem.r.insert(
p2,
RegDataType::Single(ColumnType::Single {
datatype: DataType::Int64,
@ -1224,7 +1180,7 @@ pub(super) fn explain(
state.result = Some(
(p1..p1 + p2)
.map(|i| {
let coltype = state.r.get(&i);
let coltype = state.mem.r.get(&i);
let sqltype =
coltype.map(|d| d.map_to_datatype()).map(SqliteTypeInfo);
@ -1257,11 +1213,11 @@ pub(super) fn explain(
_ => {
// ignore unsupported operations
// if we fail to find an r later, we just give up
logger.add_unknown_operation(&program[state.program_i]);
logger.add_unknown_operation(&program[state.mem.program_i]);
}
}
state.program_i += 1;
state.mem.program_i += 1;
}
}

View file

@ -0,0 +1,115 @@
/// Simplistic map implementation built on a Vec of Options (index = key)
#[derive(Debug, Clone, Eq, Default)]
pub(crate) struct IntMap<V: std::fmt::Debug + Clone + Eq + PartialEq + std::hash::Hash>(
Vec<Option<V>>,
);
impl<V: std::fmt::Debug + Clone + Eq + PartialEq + std::hash::Hash> IntMap<V> {
pub(crate) fn new() -> Self {
Self(Vec::new())
}
pub(crate) fn expand(&mut self, size: i64) -> usize {
let idx = size.try_into().expect("negative column index unsupported");
while self.0.len() <= idx {
self.0.push(None);
}
idx
}
pub(crate) fn from_dense_record(record: &Vec<V>) -> Self {
Self(record.iter().cloned().map(Some).collect())
}
pub(crate) fn values_mut(&mut self) -> impl Iterator<Item = &mut V> {
self.0.iter_mut().filter_map(Option::as_mut)
}
pub(crate) fn values(&self) -> impl Iterator<Item = &V> {
self.0.iter().filter_map(Option::as_ref)
}
pub(crate) fn get(&self, idx: &i64) -> Option<&V> {
let idx: usize = (*idx)
.try_into()
.expect("negative column index unsupported");
match self.0.get(idx) {
Some(Some(v)) => Some(v),
_ => None,
}
}
pub(crate) fn get_mut(&mut self, idx: &i64) -> Option<&mut V> {
let idx: usize = (*idx)
.try_into()
.expect("negative column index unsupported");
match self.0.get_mut(idx) {
Some(Some(v)) => Some(v),
_ => None,
}
}
pub(crate) fn insert(&mut self, idx: i64, value: V) -> Option<V> {
let idx: usize = self.expand(idx);
std::mem::replace(&mut self.0[idx], Some(value))
}
pub(crate) fn remove(&mut self, idx: &i64) -> Option<V> {
let idx: usize = (*idx)
.try_into()
.expect("negative column index unsupported");
let item = self.0.get_mut(idx);
match item {
Some(content) => std::mem::replace(content, None),
None => None,
}
}
}
impl<V: std::fmt::Debug + Clone + Eq + PartialEq + std::hash::Hash> std::hash::Hash for IntMap<V> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
for value in self.values() {
value.hash(state);
}
}
}
impl<V: std::fmt::Debug + Clone + Eq + PartialEq + std::hash::Hash> PartialEq for IntMap<V> {
fn eq(&self, other: &Self) -> bool {
if !self
.0
.iter()
.zip(other.0.iter())
.all(|(l, r)| PartialEq::eq(l, r))
{
return false;
}
if self.0.len() > other.0.len() {
self.0[other.0.len()..].iter().all(Option::is_none)
} else if self.0.len() < other.0.len() {
other.0[self.0.len()..].iter().all(Option::is_none)
} else {
true
}
}
}
impl<V: std::fmt::Debug + Clone + Eq + PartialEq + std::hash::Hash + Default> FromIterator<(i64, V)>
for IntMap<V>
{
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = (i64, V)>,
{
let mut result = Self(Vec::new());
for (idx, val) in iter {
let idx = result.expand(idx);
result.0[idx] = Some(val);
}
result
}
}

View file

@ -30,6 +30,7 @@ pub(crate) mod execute;
mod executor;
mod explain;
mod handle;
mod intmap;
mod worker;