mirror of
https://github.com/launchbadge/sqlx.git
synced 2025-10-02 15:25:32 +00:00
Sqlite explain graph (#3064)
* convert logger to output a query graph * avoid duplicating branch paths to shrink output graph * separate different branching paths * include all branches which found unique states * track the reason for ending each branches execution * track the result type of each branch * make edges rely on history index instead of program_id, to avoid errors when looping * add state diff to query graph * drop redundant table info * rework graph to show state changes, rework logger to store state snapshots * show state on the previous operation * gather duplicate state changes into clusters to reduce repetition * draw invisible connections between unknown instructions by program_i * clean up dot format string escaping * add test case from #1960 (update returning all columns) * add tests for #2939 (update returning only the PK column) * allow inserting into a table using only the index * improve null handling of IfNull, fix output type of NewRowId * add NoResult nodes for branches which don't log a result, as a sanity check * add short-circuit to all logging operations * remove duplicate logging checks, and make logging enabled/disabled consistently depend on sqlx::explain instead of sqlx for capture & sqlx::explain for output * add failing test for awkwardly nested/filtered count subquery * handle special case of return operation to fix failing test * require trace log level instead of using whatever log level statement logging was configured to use
This commit is contained in:
parent
2df770a10b
commit
f960d5bc3b
@ -2,10 +2,11 @@ use crate::connection::intmap::IntMap;
|
|||||||
use crate::connection::{execute, ConnectionState};
|
use crate::connection::{execute, ConnectionState};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::from_row::FromRow;
|
use crate::from_row::FromRow;
|
||||||
|
use crate::logger::{BranchParent, BranchResult, DebugDiff};
|
||||||
use crate::type_info::DataType;
|
use crate::type_info::DataType;
|
||||||
use crate::SqliteTypeInfo;
|
use crate::SqliteTypeInfo;
|
||||||
use sqlx_core::HashMap;
|
use sqlx_core::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::fmt::Debug;
|
||||||
use std::str::from_utf8;
|
use std::str::from_utf8;
|
||||||
|
|
||||||
// affinity
|
// affinity
|
||||||
@ -132,7 +133,7 @@ const OP_HALT_IF_NULL: &str = "HaltIfNull";
|
|||||||
const MAX_LOOP_COUNT: u8 = 2;
|
const MAX_LOOP_COUNT: u8 = 2;
|
||||||
const MAX_TOTAL_INSTRUCTION_COUNT: u32 = 100_000;
|
const MAX_TOTAL_INSTRUCTION_COUNT: u32 = 100_000;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||||
enum ColumnType {
|
enum ColumnType {
|
||||||
Single {
|
Single {
|
||||||
datatype: DataType,
|
datatype: DataType,
|
||||||
@ -171,6 +172,32 @@ impl ColumnType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Debug for ColumnType {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Single { datatype, nullable } => {
|
||||||
|
let nullable_str = match nullable {
|
||||||
|
Some(true) => "NULL",
|
||||||
|
Some(false) => "NOT NULL",
|
||||||
|
None => "NULL?",
|
||||||
|
};
|
||||||
|
write!(f, "{:?} {}", datatype, nullable_str)
|
||||||
|
}
|
||||||
|
Self::Record(columns) => {
|
||||||
|
f.write_str("Record(")?;
|
||||||
|
let mut column_iter = columns.iter();
|
||||||
|
if let Some(item) = column_iter.next() {
|
||||||
|
write!(f, "{:?}", item)?;
|
||||||
|
while let Some(item) = column_iter.next() {
|
||||||
|
write!(f, ", {:?}", item)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.write_str(")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
enum RegDataType {
|
enum RegDataType {
|
||||||
Single(ColumnType),
|
Single(ColumnType),
|
||||||
@ -326,7 +353,7 @@ fn opcode_to_type(op: &str) -> DataType {
|
|||||||
OP_REAL => DataType::Float,
|
OP_REAL => DataType::Float,
|
||||||
OP_BLOB => DataType::Blob,
|
OP_BLOB => DataType::Blob,
|
||||||
OP_AND | OP_OR => DataType::Bool,
|
OP_AND | OP_OR => DataType::Bool,
|
||||||
OP_ROWID | OP_COUNT | OP_INT64 | OP_INTEGER => DataType::Integer,
|
OP_NEWROWID | OP_ROWID | OP_COUNT | OP_INT64 | OP_INTEGER => DataType::Integer,
|
||||||
OP_STRING8 => DataType::Text,
|
OP_STRING8 => DataType::Text,
|
||||||
OP_COLUMN | _ => DataType::Null,
|
OP_COLUMN | _ => DataType::Null,
|
||||||
}
|
}
|
||||||
@ -376,18 +403,78 @@ fn root_block_columns(
|
|||||||
return Ok(row_info);
|
return Ok(row_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
struct Sequence(i64);
|
||||||
|
|
||||||
|
impl Sequence {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(0)
|
||||||
|
}
|
||||||
|
pub fn next(&mut self) -> i64 {
|
||||||
|
let curr = self.0;
|
||||||
|
self.0 += 1;
|
||||||
|
curr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct QueryState {
|
struct QueryState {
|
||||||
// The number of times each instruction has been visited
|
// The number of times each instruction has been visited
|
||||||
pub visited: Vec<u8>,
|
pub visited: Vec<u8>,
|
||||||
// A log of the order of execution of each instruction
|
// A unique identifier of the query branch
|
||||||
pub history: Vec<usize>,
|
pub branch_id: i64,
|
||||||
|
// How many instructions have been executed on this branch (NOT the same as program_i, which is the currently executing instruction of the program)
|
||||||
|
pub instruction_counter: i64,
|
||||||
|
// Parent branch this branch was forked from (if any)
|
||||||
|
pub branch_parent: Option<BranchParent>,
|
||||||
// State of the virtual machine
|
// State of the virtual machine
|
||||||
pub mem: MemoryState,
|
pub mem: MemoryState,
|
||||||
// Results published by the execution
|
// Results published by the execution
|
||||||
pub result: Option<Vec<(Option<SqliteTypeInfo>, Option<bool>)>>,
|
pub result: Option<Vec<(Option<SqliteTypeInfo>, Option<bool>)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&QueryState> for MemoryState {
|
||||||
|
fn from(val: &QueryState) -> Self {
|
||||||
|
val.mem.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<QueryState> for MemoryState {
|
||||||
|
fn from(val: QueryState) -> Self {
|
||||||
|
val.mem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&QueryState> for BranchParent {
|
||||||
|
fn from(val: &QueryState) -> Self {
|
||||||
|
Self {
|
||||||
|
id: val.branch_id,
|
||||||
|
idx: val.instruction_counter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueryState {
|
||||||
|
fn get_reference(&self) -> BranchParent {
|
||||||
|
BranchParent {
|
||||||
|
id: self.branch_id,
|
||||||
|
idx: self.instruction_counter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn new_branch(&self, branch_seq: &mut Sequence) -> Self {
|
||||||
|
Self {
|
||||||
|
visited: self.visited.clone(),
|
||||||
|
branch_id: branch_seq.next(),
|
||||||
|
instruction_counter: 0,
|
||||||
|
branch_parent: Some(BranchParent {
|
||||||
|
id: self.branch_id,
|
||||||
|
idx: self.instruction_counter - 1, //instruction counter is incremented at the start of processing an instruction, so need to subtract 1 to get the 'current' instruction
|
||||||
|
}),
|
||||||
|
mem: self.mem.clone(),
|
||||||
|
result: self.result.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
struct MemoryState {
|
struct MemoryState {
|
||||||
// Next instruction to execute
|
// Next instruction to execute
|
||||||
@ -400,22 +487,65 @@ struct MemoryState {
|
|||||||
pub t: IntMap<TableDataType>,
|
pub t: IntMap<TableDataType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DebugDiff for MemoryState {
|
||||||
|
fn diff(&self, prev: &Self) -> String {
|
||||||
|
let r_diff = self.r.diff(&prev.r);
|
||||||
|
let p_diff = self.p.diff(&prev.p);
|
||||||
|
let t_diff = self.t.diff(&prev.t);
|
||||||
|
|
||||||
|
let mut differences = String::new();
|
||||||
|
for (i, v) in r_diff {
|
||||||
|
if !differences.is_empty() {
|
||||||
|
differences.push('\n');
|
||||||
|
}
|
||||||
|
differences.push_str(&format!("r[{}]={:?}", i, v))
|
||||||
|
}
|
||||||
|
for (i, v) in p_diff {
|
||||||
|
if !differences.is_empty() {
|
||||||
|
differences.push('\n');
|
||||||
|
}
|
||||||
|
differences.push_str(&format!("p[{}]={:?}", i, v))
|
||||||
|
}
|
||||||
|
for (i, v) in t_diff {
|
||||||
|
if !differences.is_empty() {
|
||||||
|
differences.push('\n');
|
||||||
|
}
|
||||||
|
differences.push_str(&format!("t[{}]={:?}", i, v))
|
||||||
|
}
|
||||||
|
differences
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct BranchList {
|
struct BranchList {
|
||||||
states: Vec<QueryState>,
|
states: Vec<QueryState>,
|
||||||
visited_branch_state: HashSet<MemoryState>,
|
visited_branch_state: HashMap<MemoryState, BranchParent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BranchList {
|
impl BranchList {
|
||||||
pub fn new(state: QueryState) -> Self {
|
pub fn new(state: QueryState) -> Self {
|
||||||
Self {
|
Self {
|
||||||
states: vec![state],
|
states: vec![state],
|
||||||
visited_branch_state: HashSet::new(),
|
visited_branch_state: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn push(&mut self, state: QueryState) {
|
pub fn push<R: Debug, P: Debug>(
|
||||||
if !self.visited_branch_state.contains(&state.mem) {
|
&mut self,
|
||||||
self.visited_branch_state.insert(state.mem.clone());
|
mut state: QueryState,
|
||||||
self.states.push(state);
|
logger: &mut crate::logger::QueryPlanLogger<'_, R, MemoryState, P>,
|
||||||
|
) {
|
||||||
|
logger.add_branch(&state, &state.branch_parent.unwrap());
|
||||||
|
match self.visited_branch_state.entry(state.mem) {
|
||||||
|
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||||
|
//this state is not identical to another state, so it will need to be processed
|
||||||
|
state.mem = entry.key().clone(); //replace state.mem since .entry() moved it
|
||||||
|
entry.insert(state.get_reference());
|
||||||
|
self.states.push(state);
|
||||||
|
}
|
||||||
|
std::collections::hash_map::Entry::Occupied(entry) => {
|
||||||
|
//already saw a state identical to this one, so no point in processing it
|
||||||
|
state.mem = entry.key().clone(); //replace state.mem since .entry() moved it
|
||||||
|
logger.add_result(state, BranchResult::Dedup(entry.get().clone()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn pop(&mut self) -> Option<QueryState> {
|
pub fn pop(&mut self) -> Option<QueryState> {
|
||||||
@ -436,12 +566,13 @@ pub(super) fn explain(
|
|||||||
.collect::<Result<Vec<_>, Error>>()?;
|
.collect::<Result<Vec<_>, Error>>()?;
|
||||||
let program_size = program.len();
|
let program_size = program.len();
|
||||||
|
|
||||||
let mut logger =
|
let mut logger = crate::logger::QueryPlanLogger::new(query, &program);
|
||||||
crate::logger::QueryPlanLogger::new(query, &program, conn.log_settings.clone());
|
let mut branch_seq = Sequence::new();
|
||||||
|
|
||||||
let mut states = BranchList::new(QueryState {
|
let mut states = BranchList::new(QueryState {
|
||||||
visited: vec![0; program_size],
|
visited: vec![0; program_size],
|
||||||
history: Vec::new(),
|
branch_id: branch_seq.next(),
|
||||||
|
branch_parent: None,
|
||||||
|
instruction_counter: 0,
|
||||||
result: None,
|
result: None,
|
||||||
mem: MemoryState {
|
mem: MemoryState {
|
||||||
program_i: 0,
|
program_i: 0,
|
||||||
@ -457,22 +588,20 @@ pub(super) fn explain(
|
|||||||
while let Some(mut state) = states.pop() {
|
while let Some(mut state) = states.pop() {
|
||||||
while state.mem.program_i < program_size {
|
while state.mem.program_i < program_size {
|
||||||
let (_, ref opcode, p1, p2, p3, ref p4) = program[state.mem.program_i];
|
let (_, ref opcode, p1, p2, p3, ref p4) = program[state.mem.program_i];
|
||||||
state.history.push(state.mem.program_i);
|
|
||||||
|
logger.add_operation(state.mem.program_i, &state);
|
||||||
|
state.instruction_counter += 1;
|
||||||
|
|
||||||
//limit the number of 'instructions' that can be evaluated
|
//limit the number of 'instructions' that can be evaluated
|
||||||
if gas > 0 {
|
if gas > 0 {
|
||||||
gas -= 1;
|
gas -= 1;
|
||||||
} else {
|
} else {
|
||||||
|
logger.add_result(state, BranchResult::GasLimit);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.visited[state.mem.program_i] > MAX_LOOP_COUNT {
|
if state.visited[state.mem.program_i] > MAX_LOOP_COUNT {
|
||||||
if logger.log_enabled() {
|
logger.add_result(state, BranchResult::LoopLimit);
|
||||||
let program_history: Vec<&(i64, String, i64, i64, i64, Vec<u8>)> =
|
|
||||||
state.history.iter().map(|i| &program[*i]).collect();
|
|
||||||
logger.add_result((program_history, None));
|
|
||||||
}
|
|
||||||
|
|
||||||
//avoid (infinite) loops by breaking if we ever hit the same instruction twice
|
//avoid (infinite) loops by breaking if we ever hit the same instruction twice
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -513,23 +642,63 @@ pub(super) fn explain(
|
|||||||
OP_DECR_JUMP_ZERO | OP_ELSE_EQ | OP_EQ | OP_FILTER | OP_FOUND | OP_GE | OP_GT
|
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_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_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_INCR_VACUUM | OP_IS_NULL_OR_TYPE | OP_LE | OP_LT | OP_NE | OP_NEXT
|
||||||
| OP_NEXT | OP_NO_CONFLICT | OP_NOT_EXISTS | OP_ONCE | OP_PREV | OP_PROGRAM
|
| 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
|
| OP_ROW_SET_READ | OP_ROW_SET_TEST | OP_SEEK_GE | OP_SEEK_GT | OP_SEEK_LE
|
||||||
| OP_SEEK_LT | OP_SEEK_ROW_ID | OP_SEEK_SCAN | OP_SEQUENCE_TEST
|
| OP_SEEK_LT | OP_SEEK_ROW_ID | OP_SEEK_SCAN | OP_SEQUENCE_TEST
|
||||||
| OP_SORTER_NEXT | OP_V_FILTER | OP_V_NEXT => {
|
| OP_SORTER_NEXT | OP_V_FILTER | OP_V_NEXT => {
|
||||||
// goto <p2> or next instruction (depending on actual values)
|
// goto <p2> or next instruction (depending on actual values)
|
||||||
|
|
||||||
let mut branch_state = state.clone();
|
let mut branch_state = state.new_branch(&mut branch_seq);
|
||||||
branch_state.mem.program_i = p2 as usize;
|
branch_state.mem.program_i = p2 as usize;
|
||||||
states.push(branch_state);
|
states.push(branch_state, &mut logger);
|
||||||
|
|
||||||
state.mem.program_i += 1;
|
state.mem.program_i += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OP_IS_NULL => {
|
||||||
|
// goto <p2> if p1 is null
|
||||||
|
|
||||||
|
//branch if maybe null
|
||||||
|
let might_branch = match state.mem.r.get(&p1) {
|
||||||
|
Some(r_p1) => !matches!(r_p1.map_to_nullable(), Some(false)),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
//nobranch if maybe not null
|
||||||
|
let might_not_branch = match state.mem.r.get(&p1) {
|
||||||
|
Some(r_p1) => !matches!(r_p1.map_to_datatype(), DataType::Null),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if might_branch {
|
||||||
|
let mut branch_state = state.new_branch(&mut branch_seq);
|
||||||
|
branch_state.mem.program_i = p2 as usize;
|
||||||
|
branch_state
|
||||||
|
.mem
|
||||||
|
.r
|
||||||
|
.insert(p1, RegDataType::Single(ColumnType::default()));
|
||||||
|
|
||||||
|
states.push(branch_state, &mut logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
if might_not_branch {
|
||||||
|
state.mem.program_i += 1;
|
||||||
|
if let Some(RegDataType::Single(ColumnType::Single { nullable, .. })) =
|
||||||
|
state.mem.r.get_mut(&p1)
|
||||||
|
{
|
||||||
|
*nullable = Some(false);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
logger.add_result(state, BranchResult::Branched);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OP_NOT_NULL => {
|
OP_NOT_NULL => {
|
||||||
// goto <p2> or next instruction (depending on actual values)
|
// goto <p2> if p1 is not null
|
||||||
|
|
||||||
let might_branch = match state.mem.r.get(&p1) {
|
let might_branch = match state.mem.r.get(&p1) {
|
||||||
Some(r_p1) => !matches!(r_p1.map_to_datatype(), DataType::Null),
|
Some(r_p1) => !matches!(r_p1.map_to_datatype(), DataType::Null),
|
||||||
@ -542,7 +711,7 @@ pub(super) fn explain(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if might_branch {
|
if might_branch {
|
||||||
let mut branch_state = state.clone();
|
let mut branch_state = state.new_branch(&mut branch_seq);
|
||||||
branch_state.mem.program_i = p2 as usize;
|
branch_state.mem.program_i = p2 as usize;
|
||||||
if let Some(RegDataType::Single(ColumnType::Single { nullable, .. })) =
|
if let Some(RegDataType::Single(ColumnType::Single { nullable, .. })) =
|
||||||
branch_state.mem.r.get_mut(&p1)
|
branch_state.mem.r.get_mut(&p1)
|
||||||
@ -550,7 +719,7 @@ pub(super) fn explain(
|
|||||||
*nullable = Some(false);
|
*nullable = Some(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
states.push(branch_state);
|
states.push(branch_state, &mut logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
if might_not_branch {
|
if might_not_branch {
|
||||||
@ -561,6 +730,7 @@ pub(super) fn explain(
|
|||||||
.insert(p1, RegDataType::Single(ColumnType::default()));
|
.insert(p1, RegDataType::Single(ColumnType::default()));
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
|
logger.add_result(state, BranchResult::Branched);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -571,9 +741,9 @@ pub(super) fn explain(
|
|||||||
|
|
||||||
//don't bother checking actual types, just don't branch to instruction 0
|
//don't bother checking actual types, just don't branch to instruction 0
|
||||||
if p2 != 0 {
|
if p2 != 0 {
|
||||||
let mut branch_state = state.clone();
|
let mut branch_state = state.new_branch(&mut branch_seq);
|
||||||
branch_state.mem.program_i = p2 as usize;
|
branch_state.mem.program_i = p2 as usize;
|
||||||
states.push(branch_state);
|
states.push(branch_state, &mut logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.mem.program_i += 1;
|
state.mem.program_i += 1;
|
||||||
@ -594,13 +764,13 @@ pub(super) fn explain(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if might_branch {
|
if might_branch {
|
||||||
let mut branch_state = state.clone();
|
let mut branch_state = state.new_branch(&mut branch_seq);
|
||||||
branch_state.mem.program_i = p2 as usize;
|
branch_state.mem.program_i = p2 as usize;
|
||||||
if p3 == 0 {
|
if p3 == 0 {
|
||||||
branch_state.mem.r.insert(p1, RegDataType::Int(1));
|
branch_state.mem.r.insert(p1, RegDataType::Int(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
states.push(branch_state);
|
states.push(branch_state, &mut logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
if might_not_branch {
|
if might_not_branch {
|
||||||
@ -610,6 +780,7 @@ pub(super) fn explain(
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
|
logger.add_result(state, BranchResult::Branched);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -631,12 +802,12 @@ pub(super) fn explain(
|
|||||||
|
|
||||||
let loop_detected = state.visited[state.mem.program_i] > 1;
|
let loop_detected = state.visited[state.mem.program_i] > 1;
|
||||||
if might_branch || loop_detected {
|
if might_branch || loop_detected {
|
||||||
let mut branch_state = state.clone();
|
let mut branch_state = state.new_branch(&mut branch_seq);
|
||||||
branch_state.mem.program_i = p2 as usize;
|
branch_state.mem.program_i = p2 as usize;
|
||||||
if let Some(RegDataType::Int(r_p1)) = branch_state.mem.r.get_mut(&p1) {
|
if let Some(RegDataType::Int(r_p1)) = branch_state.mem.r.get_mut(&p1) {
|
||||||
*r_p1 -= 1;
|
*r_p1 -= 1;
|
||||||
}
|
}
|
||||||
states.push(branch_state);
|
states.push(branch_state, &mut logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
if might_not_branch {
|
if might_not_branch {
|
||||||
@ -656,6 +827,7 @@ pub(super) fn explain(
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
|
logger.add_result(state, BranchResult::Branched);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -672,7 +844,7 @@ pub(super) fn explain(
|
|||||||
if matches!(cursor.is_empty(&state.mem.t), None | Some(true)) {
|
if matches!(cursor.is_empty(&state.mem.t), None | Some(true)) {
|
||||||
//only take this branch if the cursor is empty
|
//only take this branch if the cursor is empty
|
||||||
|
|
||||||
let mut branch_state = state.clone();
|
let mut branch_state = state.new_branch(&mut branch_seq);
|
||||||
branch_state.mem.program_i = p2 as usize;
|
branch_state.mem.program_i = p2 as usize;
|
||||||
|
|
||||||
if let Some(cur) = branch_state.mem.p.get(&p1) {
|
if let Some(cur) = branch_state.mem.p.get(&p1) {
|
||||||
@ -680,7 +852,7 @@ pub(super) fn explain(
|
|||||||
tab.is_empty = Some(true);
|
tab.is_empty = Some(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
states.push(branch_state);
|
states.push(branch_state, &mut logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches!(cursor.is_empty(&state.mem.t), None | Some(false)) {
|
if matches!(cursor.is_empty(&state.mem.t), None | Some(false)) {
|
||||||
@ -688,16 +860,12 @@ pub(super) fn explain(
|
|||||||
state.mem.program_i += 1;
|
state.mem.program_i += 1;
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
|
logger.add_result(state, BranchResult::Branched);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if logger.log_enabled() {
|
logger.add_result(state, BranchResult::Branched);
|
||||||
let program_history: Vec<&(i64, String, i64, i64, i64, Vec<u8>)> =
|
|
||||||
state.history.iter().map(|i| &program[*i]).collect();
|
|
||||||
logger.add_result((program_history, None));
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -726,34 +894,15 @@ pub(super) fn explain(
|
|||||||
state.mem.r.remove(&p1);
|
state.mem.r.remove(&p1);
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
if logger.log_enabled() {
|
logger.add_result(state, BranchResult::Error);
|
||||||
let program_history: Vec<&(
|
|
||||||
i64,
|
|
||||||
String,
|
|
||||||
i64,
|
|
||||||
i64,
|
|
||||||
i64,
|
|
||||||
Vec<u8>,
|
|
||||||
)> = state.history.iter().map(|i| &program[*i]).collect();
|
|
||||||
logger.add_result((program_history, None));
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if logger.log_enabled() {
|
logger.add_result(state, BranchResult::Error);
|
||||||
let program_history: Vec<&(i64, String, i64, i64, i64, Vec<u8>)> =
|
|
||||||
state.history.iter().map(|i| &program[*i]).collect();
|
|
||||||
logger.add_result((program_history, None));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if logger.log_enabled() {
|
logger.add_result(state, BranchResult::Error);
|
||||||
let program_history: Vec<&(i64, String, i64, i64, i64, Vec<u8>)> =
|
|
||||||
state.history.iter().map(|i| &program[*i]).collect();
|
|
||||||
logger.add_result((program_history, None));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -765,12 +914,11 @@ pub(super) fn explain(
|
|||||||
state.mem.program_i = (*return_i + 1) as usize;
|
state.mem.program_i = (*return_i + 1) as usize;
|
||||||
state.mem.r.remove(&p1);
|
state.mem.r.remove(&p1);
|
||||||
continue;
|
continue;
|
||||||
|
} else if p3 == 1 {
|
||||||
|
state.mem.program_i += 1;
|
||||||
|
continue;
|
||||||
} else {
|
} else {
|
||||||
if logger.log_enabled() {
|
logger.add_result(state, BranchResult::Error);
|
||||||
let program_history: Vec<&(i64, String, i64, i64, i64, Vec<u8>)> =
|
|
||||||
state.history.iter().map(|i| &program[*i]).collect();
|
|
||||||
logger.add_result((program_history, None));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -796,11 +944,7 @@ pub(super) fn explain(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if logger.log_enabled() {
|
logger.add_result(state, BranchResult::Error);
|
||||||
let program_history: Vec<&(i64, String, i64, i64, i64, Vec<u8>)> =
|
|
||||||
state.history.iter().map(|i| &program[*i]).collect();
|
|
||||||
logger.add_result((program_history, None));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -808,17 +952,17 @@ pub(super) fn explain(
|
|||||||
OP_JUMP => {
|
OP_JUMP => {
|
||||||
// goto one of <p1>, <p2>, or <p3> based on the result of a prior compare
|
// goto one of <p1>, <p2>, or <p3> based on the result of a prior compare
|
||||||
|
|
||||||
let mut branch_state = state.clone();
|
let mut branch_state = state.new_branch(&mut branch_seq);
|
||||||
branch_state.mem.program_i = p1 as usize;
|
branch_state.mem.program_i = p1 as usize;
|
||||||
states.push(branch_state);
|
states.push(branch_state, &mut logger);
|
||||||
|
|
||||||
let mut branch_state = state.clone();
|
let mut branch_state = state.new_branch(&mut branch_seq);
|
||||||
branch_state.mem.program_i = p2 as usize;
|
branch_state.mem.program_i = p2 as usize;
|
||||||
states.push(branch_state);
|
states.push(branch_state, &mut logger);
|
||||||
|
|
||||||
let mut branch_state = state.clone();
|
let mut branch_state = state.new_branch(&mut branch_seq);
|
||||||
branch_state.mem.program_i = p3 as usize;
|
branch_state.mem.program_i = p3 as usize;
|
||||||
states.push(branch_state);
|
states.push(branch_state, &mut logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
OP_COLUMN => {
|
OP_COLUMN => {
|
||||||
@ -889,18 +1033,35 @@ pub(super) fn explain(
|
|||||||
}
|
}
|
||||||
|
|
||||||
OP_INSERT | OP_IDX_INSERT | OP_SORTER_INSERT => {
|
OP_INSERT | OP_IDX_INSERT | OP_SORTER_INSERT => {
|
||||||
if let Some(RegDataType::Single(ColumnType::Record(record))) =
|
if let Some(RegDataType::Single(columntype)) = state.mem.r.get(&p2) {
|
||||||
state.mem.r.get(&p2)
|
match columntype {
|
||||||
{
|
ColumnType::Record(record) => {
|
||||||
if let Some(TableDataType { cols, is_empty }) = state
|
if let Some(TableDataType { cols, is_empty }) = state
|
||||||
.mem
|
.mem
|
||||||
.p
|
.p
|
||||||
.get(&p1)
|
.get(&p1)
|
||||||
.and_then(|cur| cur.table_mut(&mut state.mem.t))
|
.and_then(|cur| cur.table_mut(&mut state.mem.t))
|
||||||
{
|
{
|
||||||
// Insert the record into wherever pointer p1 is
|
// Insert the record into wherever pointer p1 is
|
||||||
*cols = record.clone();
|
*cols = record.clone();
|
||||||
*is_empty = Some(false);
|
*is_empty = Some(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ColumnType::Single {
|
||||||
|
datatype: DataType::Null,
|
||||||
|
nullable: _,
|
||||||
|
} => {
|
||||||
|
if let Some(TableDataType { is_empty, .. }) = state
|
||||||
|
.mem
|
||||||
|
.p
|
||||||
|
.get(&p1)
|
||||||
|
.and_then(|cur| cur.table_mut(&mut state.mem.t))
|
||||||
|
{
|
||||||
|
// Insert a null record into wherever pointer p1 is
|
||||||
|
*is_empty = Some(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Noop if the register p2 isn't a record, or if pointer p1 does not exist
|
//Noop if the register p2 isn't a record, or if pointer p1 does not exist
|
||||||
@ -1035,7 +1196,7 @@ pub(super) fn explain(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => logger.add_unknown_operation(&program[state.mem.program_i]),
|
_ => logger.add_unknown_operation(state.mem.program_i),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1284,44 +1445,39 @@ pub(super) fn explain(
|
|||||||
|
|
||||||
OP_RESULT_ROW => {
|
OP_RESULT_ROW => {
|
||||||
// output = r[p1 .. p1 + p2]
|
// output = r[p1 .. p1 + p2]
|
||||||
|
let result: Vec<_> = (p1..p1 + p2)
|
||||||
|
.map(|i| {
|
||||||
|
state
|
||||||
|
.mem
|
||||||
|
.r
|
||||||
|
.get(&i)
|
||||||
|
.map(RegDataType::map_to_columntype)
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
state.result = Some(
|
let mut branch_state = state.new_branch(&mut branch_seq);
|
||||||
(p1..p1 + p2)
|
branch_state.mem.program_i += 1;
|
||||||
.map(|i| {
|
states.push(branch_state, &mut logger);
|
||||||
let coltype = state.mem.r.get(&i);
|
|
||||||
|
|
||||||
let sqltype =
|
logger.add_result(
|
||||||
coltype.map(|d| d.map_to_datatype()).map(SqliteTypeInfo);
|
state,
|
||||||
let nullable =
|
BranchResult::Result(IntMap::from_dense_record(&result)),
|
||||||
coltype.map(|d| d.map_to_nullable()).unwrap_or_default();
|
|
||||||
|
|
||||||
(sqltype, nullable)
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if logger.log_enabled() {
|
result_states.push(result);
|
||||||
let program_history: Vec<&(i64, String, i64, i64, i64, Vec<u8>)> =
|
break;
|
||||||
state.history.iter().map(|i| &program[*i]).collect();
|
|
||||||
logger.add_result((program_history, Some(state.result.clone())));
|
|
||||||
}
|
|
||||||
|
|
||||||
result_states.push(state.clone());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OP_HALT => {
|
OP_HALT => {
|
||||||
if logger.log_enabled() {
|
logger.add_result(state, BranchResult::Halt);
|
||||||
let program_history: Vec<&(i64, String, i64, i64, i64, Vec<u8>)> =
|
|
||||||
state.history.iter().map(|i| &program[*i]).collect();
|
|
||||||
logger.add_result((program_history, None));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
// ignore unsupported operations
|
// ignore unsupported operations
|
||||||
// if we fail to find an r later, we just give up
|
// if we fail to find an r later, we just give up
|
||||||
logger.add_unknown_operation(&program[state.mem.program_i]);
|
logger.add_unknown_operation(state.mem.program_i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1332,31 +1488,32 @@ pub(super) fn explain(
|
|||||||
let mut output: Vec<Option<SqliteTypeInfo>> = Vec::new();
|
let mut output: Vec<Option<SqliteTypeInfo>> = Vec::new();
|
||||||
let mut nullable: Vec<Option<bool>> = Vec::new();
|
let mut nullable: Vec<Option<bool>> = Vec::new();
|
||||||
|
|
||||||
while let Some(state) = result_states.pop() {
|
while let Some(result) = result_states.pop() {
|
||||||
// find the datatype info from each ResultRow execution
|
// find the datatype info from each ResultRow execution
|
||||||
if let Some(result) = state.result {
|
let mut idx = 0;
|
||||||
let mut idx = 0;
|
for this_col in result {
|
||||||
for (this_type, this_nullable) in result {
|
let this_type = this_col.map_to_datatype();
|
||||||
if output.len() == idx {
|
let this_nullable = this_col.map_to_nullable();
|
||||||
output.push(this_type);
|
if output.len() == idx {
|
||||||
} else if output[idx].is_none()
|
output.push(Some(SqliteTypeInfo(this_type)));
|
||||||
|| matches!(output[idx], Some(SqliteTypeInfo(DataType::Null)))
|
} else if output[idx].is_none()
|
||||||
{
|
|| matches!(output[idx], Some(SqliteTypeInfo(DataType::Null)))
|
||||||
output[idx] = this_type;
|
&& !matches!(this_type, DataType::Null)
|
||||||
}
|
{
|
||||||
|
output[idx] = Some(SqliteTypeInfo(this_type));
|
||||||
if nullable.len() == idx {
|
|
||||||
nullable.push(this_nullable);
|
|
||||||
} else if let Some(ref mut null) = nullable[idx] {
|
|
||||||
//if any ResultRow's column is nullable, the final result is nullable
|
|
||||||
if let Some(this_null) = this_nullable {
|
|
||||||
*null |= this_null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nullable[idx] = this_nullable;
|
|
||||||
}
|
|
||||||
idx += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if nullable.len() == idx {
|
||||||
|
nullable.push(this_nullable);
|
||||||
|
} else if let Some(ref mut null) = nullable[idx] {
|
||||||
|
//if any ResultRow's column is nullable, the final result is nullable
|
||||||
|
if let Some(this_null) = this_nullable {
|
||||||
|
*null |= this_null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nullable[idx] = this_nullable;
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
/// Simplistic map implementation built on a Vec of Options (index = key)
|
use std::{fmt::Debug, hash::Hash};
|
||||||
#[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> {
|
/// Simplistic map implementation built on a Vec of Options (index = key)
|
||||||
|
#[derive(Debug, Clone, Eq)]
|
||||||
|
pub(crate) struct IntMap<V>(Vec<Option<V>>);
|
||||||
|
|
||||||
|
impl<V> Default for IntMap<V> {
|
||||||
|
fn default() -> Self {
|
||||||
|
IntMap(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V> IntMap<V> {
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
Self(Vec::new())
|
Self(Vec::new())
|
||||||
}
|
}
|
||||||
@ -17,10 +23,6 @@ impl<V: std::fmt::Debug + Clone + Eq + PartialEq + std::hash::Hash> IntMap<V> {
|
|||||||
idx
|
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> {
|
pub(crate) fn values_mut(&mut self) -> impl Iterator<Item = &mut V> {
|
||||||
self.0.iter_mut().filter_map(Option::as_mut)
|
self.0.iter_mut().filter_map(Option::as_mut)
|
||||||
}
|
}
|
||||||
@ -67,9 +69,67 @@ impl<V: std::fmt::Debug + Clone + Eq + PartialEq + std::hash::Hash> IntMap<V> {
|
|||||||
None => None,
|
None => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn iter(&self) -> impl Iterator<Item = Option<&V>> {
|
||||||
|
self.0.iter().map(Option::as_ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn iter_entries(&self) -> impl Iterator<Item = (i64, &V)> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, v)| v.as_ref().map(|v: &V| (i as i64, v)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn last_index(&self) -> Option<i64> {
|
||||||
|
self.0.iter().rposition(|v| v.is_some()).map(|i| i as i64)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: std::fmt::Debug + Clone + Eq + PartialEq + std::hash::Hash> std::hash::Hash for IntMap<V> {
|
impl<V: Default> IntMap<V> {
|
||||||
|
pub(crate) fn get_mut_or_default<'a>(&'a mut self, idx: &i64) -> &'a mut V {
|
||||||
|
let idx: usize = self.expand(*idx);
|
||||||
|
|
||||||
|
let item: &mut Option<V> = &mut self.0[idx];
|
||||||
|
if item.is_none() {
|
||||||
|
*item = Some(V::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.0[idx].as_mut().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: Clone> IntMap<V> {
|
||||||
|
pub(crate) fn from_dense_record(record: &Vec<V>) -> Self {
|
||||||
|
Self(record.iter().cloned().map(Some).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: Eq> IntMap<V> {
|
||||||
|
/// get the additions to this intmap compared to the prev intmap
|
||||||
|
pub(crate) fn diff<'a, 'b, 'c>(
|
||||||
|
&'a self,
|
||||||
|
prev: &'b Self,
|
||||||
|
) -> impl Iterator<Item = (usize, Option<&'c V>)>
|
||||||
|
where
|
||||||
|
'a: 'c,
|
||||||
|
'b: 'c,
|
||||||
|
{
|
||||||
|
let self_pad = if prev.0.len() > self.0.len() {
|
||||||
|
prev.0.len() - self.0.len()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
self.iter()
|
||||||
|
.chain(std::iter::repeat(None).take(self_pad))
|
||||||
|
.zip(prev.iter().chain(std::iter::repeat(None)))
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_i, (n, p))| n != p)
|
||||||
|
.map(|(i, (n, _p))| (i, n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: Hash> Hash for IntMap<V> {
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
for value in self.values() {
|
for value in self.values() {
|
||||||
value.hash(state);
|
value.hash(state);
|
||||||
@ -77,7 +137,7 @@ impl<V: std::fmt::Debug + Clone + Eq + PartialEq + std::hash::Hash> std::hash::H
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: std::fmt::Debug + Clone + Eq + PartialEq + std::hash::Hash> PartialEq for IntMap<V> {
|
impl<V: PartialEq> PartialEq for IntMap<V> {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
if !self
|
if !self
|
||||||
.0
|
.0
|
||||||
@ -98,9 +158,7 @@ impl<V: std::fmt::Debug + Clone + Eq + PartialEq + std::hash::Hash> PartialEq fo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: std::fmt::Debug + Clone + Eq + PartialEq + std::hash::Hash + Default> FromIterator<(i64, V)>
|
impl<V: Debug> FromIterator<(i64, V)> for IntMap<V> {
|
||||||
for IntMap<V>
|
|
||||||
{
|
|
||||||
fn from_iter<I>(iter: I) -> Self
|
fn from_iter<I>(iter: I) -> Self
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = (i64, V)>,
|
I: IntoIterator<Item = (i64, V)>,
|
||||||
|
@ -30,7 +30,7 @@ pub(crate) mod execute;
|
|||||||
mod executor;
|
mod executor;
|
||||||
mod explain;
|
mod explain;
|
||||||
mod handle;
|
mod handle;
|
||||||
mod intmap;
|
pub(crate) mod intmap;
|
||||||
|
|
||||||
mod worker;
|
mod worker;
|
||||||
|
|
||||||
|
@ -1,87 +1,432 @@
|
|||||||
use sqlx_core::{connection::LogSettings, logger};
|
use crate::connection::intmap::IntMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
|
|
||||||
pub(crate) use sqlx_core::logger::*;
|
pub(crate) use sqlx_core::logger::*;
|
||||||
|
|
||||||
pub struct QueryPlanLogger<'q, O: Debug + Hash + Eq, R: Debug, P: Debug> {
|
#[derive(Debug)]
|
||||||
sql: &'q str,
|
pub(crate) enum BranchResult<R: Debug + 'static> {
|
||||||
unknown_operations: HashSet<O>,
|
Result(R),
|
||||||
results: Vec<R>,
|
Dedup(BranchParent),
|
||||||
program: &'q [P],
|
Halt,
|
||||||
settings: LogSettings,
|
Error,
|
||||||
|
GasLimit,
|
||||||
|
LoopLimit,
|
||||||
|
Branched,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'q, O: Debug + Hash + Eq, R: Debug, P: Debug> QueryPlanLogger<'q, O, R, P> {
|
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, Ord, PartialOrd)]
|
||||||
pub fn new(sql: &'q str, program: &'q [P], settings: LogSettings) -> Self {
|
pub(crate) struct BranchParent {
|
||||||
|
pub id: i64,
|
||||||
|
pub idx: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct InstructionHistory<S: Debug + DebugDiff> {
|
||||||
|
pub program_i: usize,
|
||||||
|
pub state: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait DebugDiff {
|
||||||
|
fn diff(&self, prev: &Self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct QueryPlanLogger<'q, R: Debug + 'static, S: Debug + DebugDiff + 'static, P: Debug> {
|
||||||
|
sql: &'q str,
|
||||||
|
unknown_operations: HashSet<usize>,
|
||||||
|
branch_origins: IntMap<BranchParent>,
|
||||||
|
branch_results: IntMap<BranchResult<R>>,
|
||||||
|
branch_operations: IntMap<IntMap<InstructionHistory<S>>>,
|
||||||
|
program: &'q [P],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// convert a string into dot format
|
||||||
|
fn dot_escape_string(value: impl AsRef<str>) -> String {
|
||||||
|
value
|
||||||
|
.as_ref()
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "'")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Debug, S: Debug + DebugDiff, P: Debug> core::fmt::Display for QueryPlanLogger<'_, R, S, P> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
//writes query plan history in dot format
|
||||||
|
f.write_str("digraph {\n")?;
|
||||||
|
|
||||||
|
f.write_str("subgraph operations {\n")?;
|
||||||
|
f.write_str("style=\"rounded\";\nnode [shape=\"point\"];\n")?;
|
||||||
|
|
||||||
|
let all_states: std::collections::HashMap<BranchParent, &InstructionHistory<S>> = self
|
||||||
|
.branch_operations
|
||||||
|
.iter_entries()
|
||||||
|
.flat_map(
|
||||||
|
|(branch_id, instructions): (i64, &IntMap<InstructionHistory<S>>)| {
|
||||||
|
instructions.iter_entries().map(
|
||||||
|
move |(idx, ih): (i64, &InstructionHistory<S>)| {
|
||||||
|
(BranchParent { id: branch_id, idx }, ih)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut instruction_uses: IntMap<Vec<BranchParent>> = Default::default();
|
||||||
|
for (k, state) in all_states.iter() {
|
||||||
|
let entry = instruction_uses.get_mut_or_default(&(state.program_i as i64));
|
||||||
|
entry.push(k.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut branch_children: std::collections::HashMap<BranchParent, Vec<BranchParent>> =
|
||||||
|
Default::default();
|
||||||
|
|
||||||
|
let mut branched_with_state: std::collections::HashSet<BranchParent> = Default::default();
|
||||||
|
|
||||||
|
for (branch_id, branch_parent) in self.branch_origins.iter_entries() {
|
||||||
|
let entry = branch_children.entry(*branch_parent).or_default();
|
||||||
|
entry.push(BranchParent {
|
||||||
|
id: branch_id,
|
||||||
|
idx: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (idx, instruction) in self.program.iter().enumerate() {
|
||||||
|
let escaped_instruction = dot_escape_string(format!("{:?}", instruction));
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"subgraph cluster_{} {{ label=\"{}\"",
|
||||||
|
idx, escaped_instruction
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if self.unknown_operations.contains(&idx) {
|
||||||
|
f.write_str(" style=dashed")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.write_str(";\n")?;
|
||||||
|
|
||||||
|
let mut state_list: std::collections::BTreeMap<
|
||||||
|
String,
|
||||||
|
Vec<(BranchParent, Option<BranchParent>)>,
|
||||||
|
> = Default::default();
|
||||||
|
|
||||||
|
write!(f, "i{}[style=invis];", idx)?;
|
||||||
|
|
||||||
|
if let Some(this_instruction_uses) = instruction_uses.get(&(idx as i64)) {
|
||||||
|
for curr_ref in this_instruction_uses.iter() {
|
||||||
|
if let Some(curr_state) = all_states.get(curr_ref) {
|
||||||
|
let next_ref = BranchParent {
|
||||||
|
id: curr_ref.id,
|
||||||
|
idx: curr_ref.idx + 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(next_state) = all_states.get(&next_ref) {
|
||||||
|
let state_diff = next_state.state.diff(&curr_state.state);
|
||||||
|
|
||||||
|
state_list
|
||||||
|
.entry(state_diff)
|
||||||
|
.or_default()
|
||||||
|
.push((curr_ref.clone(), Some(next_ref)));
|
||||||
|
} else {
|
||||||
|
state_list
|
||||||
|
.entry(Default::default())
|
||||||
|
.or_default()
|
||||||
|
.push((curr_ref.clone(), None));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(children) = branch_children.get(curr_ref) {
|
||||||
|
for next_ref in children {
|
||||||
|
if let Some(next_state) = all_states.get(&next_ref) {
|
||||||
|
let state_diff = next_state.state.diff(&curr_state.state);
|
||||||
|
|
||||||
|
if !state_diff.is_empty() {
|
||||||
|
branched_with_state.insert(next_ref.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
state_list
|
||||||
|
.entry(state_diff)
|
||||||
|
.or_default()
|
||||||
|
.push((curr_ref.clone(), Some(next_ref.clone())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for curr_ref in this_instruction_uses {
|
||||||
|
if branch_children.contains_key(curr_ref) {
|
||||||
|
write!(f, "\"b{}p{}\";", curr_ref.id, curr_ref.idx)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
write!(f, "i{}->i{}[style=invis];", idx - 1, idx)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (state_num, (state_diff, ref_list)) in state_list.iter().enumerate() {
|
||||||
|
if !state_diff.is_empty() {
|
||||||
|
let escaped_state = dot_escape_string(state_diff);
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"subgraph \"cluster_i{}s{}\" {{\nlabel=\"{}\"\n",
|
||||||
|
idx, state_num, escaped_state
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (curr_ref, next_ref) in ref_list {
|
||||||
|
if let Some(next_ref) = next_ref {
|
||||||
|
let next_program_i = all_states
|
||||||
|
.get(&next_ref)
|
||||||
|
.map(|s| s.program_i.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if branched_with_state.contains(next_ref) {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"\"b{}p{}_b{}p{}\"[tooltip=\"next:{}\"];",
|
||||||
|
curr_ref.id,
|
||||||
|
curr_ref.idx,
|
||||||
|
next_ref.id,
|
||||||
|
next_ref.idx,
|
||||||
|
next_program_i
|
||||||
|
)?;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"\"b{}p{}\"[tooltip=\"next:{}\"];",
|
||||||
|
curr_ref.id, curr_ref.idx, next_program_i
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
write!(f, "\"b{}p{}\";", curr_ref.id, curr_ref.idx)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !state_diff.is_empty() {
|
||||||
|
f.write_str("}\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.write_str("}\n")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.write_str("};\n")?; //subgraph operations
|
||||||
|
|
||||||
|
let max_branch_id: i64 = [
|
||||||
|
self.branch_operations.last_index().unwrap_or(0),
|
||||||
|
self.branch_results.last_index().unwrap_or(0),
|
||||||
|
self.branch_results.last_index().unwrap_or(0),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
f.write_str("subgraph branches {\n")?;
|
||||||
|
for branch_id in 0..=max_branch_id {
|
||||||
|
write!(f, "subgraph b{}{{", branch_id)?;
|
||||||
|
|
||||||
|
let branch_num = branch_id as usize;
|
||||||
|
let color_names = [
|
||||||
|
"blue",
|
||||||
|
"red",
|
||||||
|
"cyan",
|
||||||
|
"yellow",
|
||||||
|
"green",
|
||||||
|
"magenta",
|
||||||
|
"orange",
|
||||||
|
"purple",
|
||||||
|
"orangered",
|
||||||
|
"sienna",
|
||||||
|
"olivedrab",
|
||||||
|
"pink",
|
||||||
|
];
|
||||||
|
let color_name_root = color_names[branch_num % color_names.len()];
|
||||||
|
let color_name_suffix = match (branch_num / color_names.len()) % 4 {
|
||||||
|
0 => "1",
|
||||||
|
1 => "4",
|
||||||
|
2 => "3",
|
||||||
|
3 => "2",
|
||||||
|
_ => "",
|
||||||
|
}; //colors are easily confused after color_names.len() * 2, and outright reused after color_names.len() * 4
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"edge [colorscheme=x11 color={}{}];",
|
||||||
|
color_name_root, color_name_suffix
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut instruction_list: Vec<(BranchParent, &InstructionHistory<S>)> = Vec::new();
|
||||||
|
if let Some(parent) = self.branch_origins.get(&branch_id) {
|
||||||
|
if let Some(parent_state) = all_states.get(parent) {
|
||||||
|
instruction_list.push((parent.clone(), parent_state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(instructions) = self.branch_operations.get(&branch_id) {
|
||||||
|
for instruction in instructions.iter_entries() {
|
||||||
|
instruction_list.push((
|
||||||
|
BranchParent {
|
||||||
|
id: branch_id,
|
||||||
|
idx: instruction.0,
|
||||||
|
},
|
||||||
|
instruction.1,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut instructions_iter = instruction_list.into_iter();
|
||||||
|
|
||||||
|
if let Some((cur_ref, _)) = instructions_iter.next() {
|
||||||
|
let mut prev_ref = cur_ref;
|
||||||
|
|
||||||
|
while let Some((cur_ref, _)) = instructions_iter.next() {
|
||||||
|
if branched_with_state.contains(&cur_ref) {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"\"b{}p{}\" -> \"b{}p{}_b{}p{}\" -> \"b{}p{}\"\n",
|
||||||
|
prev_ref.id,
|
||||||
|
prev_ref.idx,
|
||||||
|
prev_ref.id,
|
||||||
|
prev_ref.idx,
|
||||||
|
cur_ref.id,
|
||||||
|
cur_ref.idx,
|
||||||
|
cur_ref.id,
|
||||||
|
cur_ref.idx
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"\"b{}p{}\" -> \"b{}p{}\";",
|
||||||
|
prev_ref.id, prev_ref.idx, cur_ref.id, cur_ref.idx
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
prev_ref = cur_ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
//draw edge to the result of this branch
|
||||||
|
if let Some(result) = self.branch_results.get(&branch_id) {
|
||||||
|
if let BranchResult::Dedup(dedup_ref) = result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"\"b{}p{}\"->\"b{}p{}\" [style=dotted]",
|
||||||
|
prev_ref.id, prev_ref.idx, dedup_ref.id, dedup_ref.idx
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
let escaped_result = dot_escape_string(format!("{:?}", result));
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"\"b{}p{}\" ->\"{}\"; \"{}\" [shape=box];",
|
||||||
|
prev_ref.id, prev_ref.idx, escaped_result, escaped_result
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"\"b{}p{}\" ->\"NoResult\"; \"NoResult\" [shape=box];",
|
||||||
|
prev_ref.id, prev_ref.idx
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.write_str("};\n")?;
|
||||||
|
}
|
||||||
|
f.write_str("};\n")?; //branches
|
||||||
|
|
||||||
|
f.write_str("}\n")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'q, R: Debug, S: Debug + DebugDiff, P: Debug> QueryPlanLogger<'q, R, S, P> {
|
||||||
|
pub fn new(sql: &'q str, program: &'q [P]) -> Self {
|
||||||
Self {
|
Self {
|
||||||
sql,
|
sql,
|
||||||
unknown_operations: HashSet::new(),
|
unknown_operations: HashSet::new(),
|
||||||
results: Vec::new(),
|
branch_origins: IntMap::new(),
|
||||||
|
branch_results: IntMap::new(),
|
||||||
|
branch_operations: IntMap::new(),
|
||||||
program,
|
program,
|
||||||
settings,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn log_enabled(&self) -> bool {
|
pub fn log_enabled(&self) -> bool {
|
||||||
if let Some((tracing_level, log_level)) =
|
log::log_enabled!(target: "sqlx::explain", log::Level::Trace)
|
||||||
logger::private_level_filter_to_levels(self.settings.statements_level)
|
|| private_tracing_dynamic_enabled!(target: "sqlx::explain", tracing::Level::TRACE)
|
||||||
{
|
}
|
||||||
log::log_enabled!(log_level)
|
|
||||||
|| sqlx_core::private_tracing_dynamic_enabled!(tracing_level)
|
pub fn add_branch<I: Copy>(&mut self, state: I, parent: &BranchParent)
|
||||||
} else {
|
where
|
||||||
false
|
BranchParent: From<I>,
|
||||||
|
{
|
||||||
|
if !self.log_enabled() {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
let branch: BranchParent = BranchParent::from(state);
|
||||||
|
self.branch_origins.insert(branch.id, parent.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_result(&mut self, result: R) {
|
pub fn add_operation<I: Copy>(&mut self, program_i: usize, state: I)
|
||||||
self.results.push(result);
|
where
|
||||||
|
BranchParent: From<I>,
|
||||||
|
S: From<I>,
|
||||||
|
{
|
||||||
|
if !self.log_enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let branch: BranchParent = BranchParent::from(state);
|
||||||
|
let state: S = S::from(state);
|
||||||
|
self.branch_operations
|
||||||
|
.get_mut_or_default(&branch.id)
|
||||||
|
.insert(branch.idx, InstructionHistory { program_i, state });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_unknown_operation(&mut self, operation: O) {
|
pub fn add_result<I>(&mut self, state: I, result: BranchResult<R>)
|
||||||
|
where
|
||||||
|
BranchParent: for<'a> From<&'a I>,
|
||||||
|
S: From<I>,
|
||||||
|
{
|
||||||
|
if !self.log_enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let branch: BranchParent = BranchParent::from(&state);
|
||||||
|
self.branch_results.insert(branch.id, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_unknown_operation(&mut self, operation: usize) {
|
||||||
|
if !self.log_enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.unknown_operations.insert(operation);
|
self.unknown_operations.insert(operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finish(&self) {
|
pub fn finish(&self) {
|
||||||
let lvl = self.settings.statements_level;
|
if !self.log_enabled() {
|
||||||
|
return;
|
||||||
if let Some((tracing_level, log_level)) = logger::private_level_filter_to_levels(lvl) {
|
|
||||||
let log_is_enabled = log::log_enabled!(target: "sqlx::explain", log_level)
|
|
||||||
|| private_tracing_dynamic_enabled!(target: "sqlx::explain", tracing_level);
|
|
||||||
if log_is_enabled {
|
|
||||||
let mut summary = parse_query_summary(&self.sql);
|
|
||||||
|
|
||||||
let sql = if summary != self.sql {
|
|
||||||
summary.push_str(" …");
|
|
||||||
format!(
|
|
||||||
"\n\n{}\n",
|
|
||||||
sqlformat::format(
|
|
||||||
&self.sql,
|
|
||||||
&sqlformat::QueryParams::None,
|
|
||||||
sqlformat::FormatOptions::default()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let message = format!(
|
|
||||||
"{}; program:{:?}, unknown_operations:{:?}, results: {:?}{}",
|
|
||||||
summary, self.program, self.unknown_operations, self.results, sql
|
|
||||||
);
|
|
||||||
|
|
||||||
sqlx_core::private_tracing_dynamic_event!(
|
|
||||||
target: "sqlx::explain",
|
|
||||||
tracing_level,
|
|
||||||
message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut summary = parse_query_summary(&self.sql);
|
||||||
|
|
||||||
|
let sql = if summary != self.sql {
|
||||||
|
summary.push_str(" …");
|
||||||
|
format!(
|
||||||
|
"\n\n{}\n",
|
||||||
|
sqlformat::format(
|
||||||
|
&self.sql,
|
||||||
|
&sqlformat::QueryParams::None,
|
||||||
|
sqlformat::FormatOptions::default()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
sqlx_core::private_tracing_dynamic_event!(
|
||||||
|
target: "sqlx::explain",
|
||||||
|
tracing::Level::TRACE,
|
||||||
|
"{}; program:\n{}\n\n{:?}", summary, self, sql
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'q, O: Debug + Hash + Eq, R: Debug, P: Debug> Drop for QueryPlanLogger<'q, O, R, P> {
|
impl<'q, R: Debug, S: Debug + DebugDiff, P: Debug> Drop for QueryPlanLogger<'q, R, S, P> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.finish();
|
self.finish();
|
||||||
}
|
}
|
||||||
|
@ -278,6 +278,26 @@ async fn it_describes_update_with_returning() -> anyhow::Result<()> {
|
|||||||
assert_eq!(d.column(0).type_info().name(), "INTEGER");
|
assert_eq!(d.column(0).type_info().name(), "INTEGER");
|
||||||
assert_eq!(d.nullable(0), Some(false));
|
assert_eq!(d.nullable(0), Some(false));
|
||||||
|
|
||||||
|
let d = conn
|
||||||
|
.describe("UPDATE accounts SET is_active=true WHERE id=?1 RETURNING *")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(d.columns().len(), 3);
|
||||||
|
assert_eq!(d.column(0).type_info().name(), "INTEGER");
|
||||||
|
assert_eq!(d.nullable(0), Some(false));
|
||||||
|
assert_eq!(d.column(1).type_info().name(), "TEXT");
|
||||||
|
assert_eq!(d.nullable(1), Some(false));
|
||||||
|
assert_eq!(d.column(2).type_info().name(), "BOOLEAN");
|
||||||
|
//assert_eq!(d.nullable(2), Some(false)); //query analysis is allowed to notice that it is always set to true by the update
|
||||||
|
|
||||||
|
let d = conn
|
||||||
|
.describe("UPDATE accounts SET is_active=true WHERE id=?1 RETURNING id")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(d.columns().len(), 1);
|
||||||
|
assert_eq!(d.column(0).type_info().name(), "INTEGER");
|
||||||
|
assert_eq!(d.nullable(0), Some(false));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -592,6 +612,42 @@ async fn it_describes_union() -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[sqlx_macros::test]
|
||||||
|
async fn it_describes_having_group_by() -> anyhow::Result<()> {
|
||||||
|
let mut conn = new::<Sqlite>().await?;
|
||||||
|
|
||||||
|
let d = conn
|
||||||
|
.describe(
|
||||||
|
r#"
|
||||||
|
WITH tweet_reply_unq as ( --tweets with a single response
|
||||||
|
SELECT tweet_id id
|
||||||
|
FROM tweet_reply
|
||||||
|
GROUP BY tweet_id
|
||||||
|
HAVING COUNT(1) = 1
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM (
|
||||||
|
SELECT NULL
|
||||||
|
FROM tweet
|
||||||
|
JOIN tweet_reply_unq
|
||||||
|
USING (id)
|
||||||
|
WHERE tweet.owner_id = accounts.id
|
||||||
|
)
|
||||||
|
) single_reply_count
|
||||||
|
FROM accounts
|
||||||
|
WHERE id = ?1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(d.column(0).type_info().name(), "INTEGER");
|
||||||
|
assert_eq!(d.nullable(0), Some(false));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
//documents failures originally found through property testing
|
//documents failures originally found through property testing
|
||||||
#[sqlx_macros::test]
|
#[sqlx_macros::test]
|
||||||
async fn it_describes_strange_queries() -> anyhow::Result<()> {
|
async fn it_describes_strange_queries() -> anyhow::Result<()> {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user