More improvements/cleanups to vdbe around casting

This commit is contained in:
PThorpe92
2025-02-24 21:31:06 -05:00
parent 6d55cdba3b
commit b31363aecb
4 changed files with 327 additions and 145 deletions

View File

@@ -3,16 +3,17 @@ use std::{rc::Rc, sync::Arc};
use crate::{
schema::{self, Column, Schema, Type},
types::{OwnedValue, OwnedValueType},
LimboError, OpenFlags, Result, Statement, StepResult, IO,
};
pub trait RoundToPrecision {
fn round_to_precision(self, precision: f64) -> f64;
fn round_to_precision(self, precision: i32) -> f64;
}
impl RoundToPrecision for f64 {
fn round_to_precision(self, precision: f64) -> f64 {
let factor = 10f64.powf(precision);
fn round_to_precision(self, precision: i32) -> f64 {
let factor = 10f64.powi(precision);
(self * factor).round() / factor
}
}
@@ -613,6 +614,156 @@ pub fn decode_percent(uri: &str) -> String {
String::from_utf8_lossy(&decoded).to_string()
}
/// When casting a TEXT value to INTEGER, the longest possible prefix of the value that can be interpreted as an integer number
/// is extracted from the TEXT value and the remainder ignored. Any leading spaces in the TEXT value when converting from TEXT to INTEGER are ignored.
/// If there is no prefix that can be interpreted as an integer number, the result of the conversion is 0.
/// If the prefix integer is greater than +9223372036854775807 then the result of the cast is exactly +9223372036854775807.
/// Similarly, if the prefix integer is less than -9223372036854775808 then the result of the cast is exactly -9223372036854775808.
/// When casting to INTEGER, if the text looks like a floating point value with an exponent, the exponent will be ignored
/// because it is no part of the integer prefix. For example, "CAST('123e+5' AS INTEGER)" results in 123, not in 12300000.
/// The CAST operator understands decimal integers only — conversion of hexadecimal integers stops at the "x" in the "0x" prefix of the hexadecimal integer string and thus result of the CAST is always zero.
pub fn cast_text_to_integer(text: &str) -> OwnedValue {
let text = text.trim();
if text.is_empty() {
return OwnedValue::Integer(0);
}
if let Ok(i) = text.parse::<i64>() {
return OwnedValue::Integer(i);
}
let bytes = text.as_bytes();
let mut end = 0;
if bytes[0] == b'-' {
end = 1;
}
while end < bytes.len() && bytes[end].is_ascii_digit() {
end += 1;
}
text[..end]
.parse::<i64>()
.map_or(OwnedValue::Integer(0), OwnedValue::Integer)
}
/// When casting a TEXT value to REAL, the longest possible prefix of the value that can be interpreted
/// as a real number is extracted from the TEXT value and the remainder ignored. Any leading spaces in
/// the TEXT value are ignored when converging from TEXT to REAL.
/// If there is no prefix that can be interpreted as a real number, the result of the conversion is 0.0.
pub fn cast_text_to_real(text: &str) -> OwnedValue {
let trimmed = text.trim();
if trimmed.is_empty() {
return OwnedValue::Float(0.0);
}
let Ok((_, text)) = parse_numeric_str(trimmed) else {
return OwnedValue::Float(0.0);
};
text.parse::<f64>()
.map_or(OwnedValue::Float(0.0), OwnedValue::Float)
}
/// NUMERIC Casting a TEXT or BLOB value into NUMERIC yields either an INTEGER or a REAL result.
/// If the input text looks like an integer (there is no decimal point nor exponent) and the value
/// is small enough to fit in a 64-bit signed integer, then the result will be INTEGER.
/// Input text that looks like floating point (there is a decimal point and/or an exponent)
/// and the text describes a value that can be losslessly converted back and forth between IEEE 754
/// 64-bit float and a 51-bit signed integer, then the result is INTEGER. (In the previous sentence,
/// a 51-bit integer is specified since that is one bit less than the length of the mantissa of an
/// IEEE 754 64-bit float and thus provides a 1-bit of margin for the text-to-float conversion operation.)
/// Any text input that describes a value outside the range of a 64-bit signed integer yields a REAL result.
/// Casting a REAL or INTEGER value to NUMERIC is a no-op, even if a real value could be losslessly converted to an integer.
pub fn checked_cast_text_to_numeric(text: &str) -> std::result::Result<OwnedValue, ()> {
// sqlite will parse the first N digits of a string to numeric value, then determine
// whether _that_ value is more likely a real or integer value. e.g.
// '-100234-2344.23e14' evaluates to -100234 instead of -100234.0
let (kind, text) = parse_numeric_str(text)?;
match kind {
OwnedValueType::Integer => {
match text.parse::<i64>() {
Ok(i) => Ok(OwnedValue::Integer(i)),
Err(e) => {
if matches!(
e.kind(),
std::num::IntErrorKind::PosOverflow | std::num::IntErrorKind::NegOverflow
) {
// if overflow, we return the representation as a real:
// we have to match sqlite exactly here, so we match sqlite3AtoF
let value = text.parse::<f64>().unwrap_or_default();
let factor = 10f64.powi(15 - value.abs().log10().ceil() as i32);
Ok(OwnedValue::Float((value * factor).round() / factor))
} else {
Err(())
}
}
}
}
OwnedValueType::Float => Ok(text
.parse::<f64>()
.map_or(OwnedValue::Float(0.0), OwnedValue::Float)),
_ => unreachable!(),
}
}
fn parse_numeric_str(text: &str) -> Result<(OwnedValueType, &str), ()> {
let bytes = text.trim_start().as_bytes();
if bytes.is_empty() {
return Err(());
}
let mut end = 0;
let mut has_decimal = false;
let mut has_exponent = false;
if bytes[0] == b'-' {
end = 1;
}
while end < bytes.len() {
match bytes[end] {
b'0'..=b'9' => end += 1,
b'.' if !has_decimal && !has_exponent => {
has_decimal = true;
end += 1;
}
b'e' | b'E' if !has_exponent => {
has_exponent = true;
end += 1;
// allow exponent sign
if end < bytes.len() && (bytes[end] == b'+' || bytes[end] == b'-') {
end += 1;
}
}
_ => break,
}
}
if end == 0 || (end == 1 && bytes[0] == b'-') {
return Err(());
}
// edge case: if it ends with exponent, strip and cast valid digits as float
let last = bytes[end - 1];
if last.eq_ignore_ascii_case(&b'e') {
return Ok((OwnedValueType::Float, &text[0..end - 1]));
// edge case: ends with extponent / sign
} else if has_exponent && (last == b'-' || last == b'+') {
return Ok((OwnedValueType::Float, &text[0..end - 2]));
}
Ok((
if !has_decimal && !has_exponent {
OwnedValueType::Integer
} else {
OwnedValueType::Float
},
&text[..end],
))
}
pub fn cast_text_to_numeric(txt: &str) -> OwnedValue {
checked_cast_text_to_numeric(txt).unwrap_or(OwnedValue::Integer(0))
}
// Check if float can be losslessly converted to 51-bit integer
pub fn cast_real_to_integer(float: f64) -> std::result::Result<i64, ()> {
let i = float as i64;
if float == i as f64 && i.abs() < (1i64 << 51) {
return Ok(i);
}
Err(())
}
#[cfg(test)]
pub mod tests {
use super::*;
@@ -1163,4 +1314,163 @@ pub mod tests {
"/home/user/db.sqlite"
);
}
#[test]
fn test_text_to_integer() {
assert_eq!(cast_text_to_integer("1"), OwnedValue::Integer(1),);
assert_eq!(cast_text_to_integer("-1"), OwnedValue::Integer(-1),);
assert_eq!(
cast_text_to_integer("1823400-00000"),
OwnedValue::Integer(1823400),
);
assert_eq!(
cast_text_to_integer("-10000000"),
OwnedValue::Integer(-10000000),
);
assert_eq!(cast_text_to_integer("123xxx"), OwnedValue::Integer(123),);
assert_eq!(
cast_text_to_integer("9223372036854775807"),
OwnedValue::Integer(i64::MAX),
);
assert_eq!(
cast_text_to_integer("9223372036854775808"),
OwnedValue::Integer(0),
);
assert_eq!(
cast_text_to_integer("-9223372036854775808"),
OwnedValue::Integer(i64::MIN),
);
assert_eq!(
cast_text_to_integer("-9223372036854775809"),
OwnedValue::Integer(0),
);
assert_eq!(cast_text_to_integer("-"), OwnedValue::Integer(0),);
}
#[test]
fn test_text_to_real() {
assert_eq!(cast_text_to_real("1"), OwnedValue::Float(1.0));
assert_eq!(cast_text_to_real("-1"), OwnedValue::Float(-1.0));
assert_eq!(cast_text_to_real("1.0"), OwnedValue::Float(1.0));
assert_eq!(cast_text_to_real("-1.0"), OwnedValue::Float(-1.0));
assert_eq!(cast_text_to_real("1e10"), OwnedValue::Float(1e10));
assert_eq!(cast_text_to_real("-1e10"), OwnedValue::Float(-1e10));
assert_eq!(cast_text_to_real("1e-10"), OwnedValue::Float(1e-10));
assert_eq!(cast_text_to_real("-1e-10"), OwnedValue::Float(-1e-10));
assert_eq!(cast_text_to_real("1.123e10"), OwnedValue::Float(1.123e10));
assert_eq!(cast_text_to_real("-1.123e10"), OwnedValue::Float(-1.123e10));
assert_eq!(cast_text_to_real("1.123e-10"), OwnedValue::Float(1.123e-10));
assert_eq!(cast_text_to_real("-1.123-e-10"), OwnedValue::Float(-1.123));
assert_eq!(cast_text_to_real("1-282584294928"), OwnedValue::Float(1.0));
assert_eq!(
cast_text_to_real("1.7976931348623157e309"),
OwnedValue::Float(f64::INFINITY),
);
assert_eq!(
cast_text_to_real("-1.7976931348623157e308"),
OwnedValue::Float(f64::MIN),
);
assert_eq!(
cast_text_to_real("-1.7976931348623157e309"),
OwnedValue::Float(f64::NEG_INFINITY),
);
assert_eq!(cast_text_to_real("1E"), OwnedValue::Float(1.0));
assert_eq!(cast_text_to_real("1EE"), OwnedValue::Float(1.0));
assert_eq!(cast_text_to_real("-1E"), OwnedValue::Float(-1.0));
assert_eq!(cast_text_to_real("1."), OwnedValue::Float(1.0));
assert_eq!(cast_text_to_real("-1."), OwnedValue::Float(-1.0));
assert_eq!(cast_text_to_real("1.23E"), OwnedValue::Float(1.23));
assert_eq!(cast_text_to_real(".1.23E-"), OwnedValue::Float(0.1));
assert_eq!(cast_text_to_real("0"), OwnedValue::Float(0.0));
assert_eq!(cast_text_to_real("-0"), OwnedValue::Float(0.0));
assert_eq!(cast_text_to_real("-0"), OwnedValue::Float(0.0));
assert_eq!(cast_text_to_real("-0.0"), OwnedValue::Float(0.0));
assert_eq!(cast_text_to_real("0.0"), OwnedValue::Float(0.0));
assert_eq!(cast_text_to_real("-"), OwnedValue::Float(0.0));
}
#[test]
fn test_text_to_numeric() {
assert_eq!(cast_text_to_numeric("1"), OwnedValue::Integer(1));
assert_eq!(cast_text_to_numeric("-1"), OwnedValue::Integer(-1));
assert_eq!(
cast_text_to_numeric("1823400-00000"),
OwnedValue::Integer(1823400)
);
assert_eq!(
cast_text_to_numeric("-10000000"),
OwnedValue::Integer(-10000000)
);
assert_eq!(cast_text_to_numeric("123xxx"), OwnedValue::Integer(123));
assert_eq!(
cast_text_to_numeric("9223372036854775807"),
OwnedValue::Integer(i64::MAX)
);
assert_eq!(
cast_text_to_numeric("9223372036854775808"),
OwnedValue::Float(9.22337203685478e18)
); // Exceeds i64, becomes float
assert_eq!(
cast_text_to_numeric("-9223372036854775808"),
OwnedValue::Integer(i64::MIN)
);
assert_eq!(
cast_text_to_numeric("-9223372036854775809"),
OwnedValue::Float(-9.22337203685478e18)
); // Exceeds i64, becomes float
assert_eq!(cast_text_to_numeric("1.0"), OwnedValue::Float(1.0));
assert_eq!(cast_text_to_numeric("-1.0"), OwnedValue::Float(-1.0));
assert_eq!(cast_text_to_numeric("1e10"), OwnedValue::Float(1e10));
assert_eq!(cast_text_to_numeric("-1e10"), OwnedValue::Float(-1e10));
assert_eq!(cast_text_to_numeric("1e-10"), OwnedValue::Float(1e-10));
assert_eq!(cast_text_to_numeric("-1e-10"), OwnedValue::Float(-1e-10));
assert_eq!(
cast_text_to_numeric("1.123e10"),
OwnedValue::Float(1.123e10)
);
assert_eq!(
cast_text_to_numeric("-1.123e10"),
OwnedValue::Float(-1.123e10)
);
assert_eq!(
cast_text_to_numeric("1.123e-10"),
OwnedValue::Float(1.123e-10)
);
assert_eq!(
cast_text_to_numeric("-1.123-e-10"),
OwnedValue::Float(-1.123)
);
assert_eq!(
cast_text_to_numeric("1-282584294928"),
OwnedValue::Integer(1)
);
assert_eq!(cast_text_to_numeric("xxx"), OwnedValue::Integer(0));
assert_eq!(
cast_text_to_numeric("1.7976931348623157e309"),
OwnedValue::Float(f64::INFINITY)
);
assert_eq!(
cast_text_to_numeric("-1.7976931348623157e308"),
OwnedValue::Float(f64::MIN)
);
assert_eq!(
cast_text_to_numeric("-1.7976931348623157e309"),
OwnedValue::Float(f64::NEG_INFINITY)
);
assert_eq!(cast_text_to_numeric("1E"), OwnedValue::Float(1.0));
assert_eq!(cast_text_to_numeric("1EE"), OwnedValue::Float(1.0));
assert_eq!(cast_text_to_numeric("-1E"), OwnedValue::Float(-1.0));
assert_eq!(cast_text_to_numeric("1."), OwnedValue::Float(1.0));
assert_eq!(cast_text_to_numeric("-1."), OwnedValue::Float(-1.0));
assert_eq!(cast_text_to_numeric("1.23E"), OwnedValue::Float(1.23));
assert_eq!(cast_text_to_numeric("1.23E-"), OwnedValue::Float(1.23));
assert_eq!(cast_text_to_numeric("0"), OwnedValue::Integer(0));
assert_eq!(cast_text_to_numeric("-0"), OwnedValue::Integer(0));
assert_eq!(cast_text_to_numeric("-0.0"), OwnedValue::Float(0.0));
assert_eq!(cast_text_to_numeric("0.0"), OwnedValue::Float(0.0));
assert_eq!(cast_text_to_numeric("-"), OwnedValue::Integer(0));
}
}

View File

@@ -3,7 +3,6 @@ use std::num::NonZero;
use super::{cast_text_to_numeric, AggFunc, BranchOffset, CursorID, FuncCtx, PageIdx};
use crate::storage::wal::CheckpointMode;
use crate::types::{OwnedValue, Record};
use crate::util::RoundToPrecision;
use limbo_macros::Description;
macro_rules! final_agg_values {
@@ -712,9 +711,7 @@ pub fn exec_add(mut lhs: &OwnedValue, mut rhs: &OwnedValue) -> OwnedValue {
OwnedValue::Integer(result.0)
}
}
(OwnedValue::Float(lhs), OwnedValue::Float(rhs)) => {
OwnedValue::Float((lhs + rhs).round_to_precision(6.0))
}
(OwnedValue::Float(lhs), OwnedValue::Float(rhs)) => OwnedValue::Float(lhs + rhs),
(OwnedValue::Float(f), OwnedValue::Integer(i))
| (OwnedValue::Integer(i), OwnedValue::Float(f)) => OwnedValue::Float(*f + *i as f64),
(OwnedValue::Null, _) | (_, OwnedValue::Null) => OwnedValue::Null,
@@ -757,6 +754,7 @@ pub fn exec_subtract(mut lhs: &OwnedValue, mut rhs: &OwnedValue) -> OwnedValue {
_ => todo!(),
}
}
pub fn exec_multiply(mut lhs: &OwnedValue, mut rhs: &OwnedValue) -> OwnedValue {
final_agg_values!(lhs, rhs);
match (lhs, rhs) {
@@ -768,9 +766,7 @@ pub fn exec_multiply(mut lhs: &OwnedValue, mut rhs: &OwnedValue) -> OwnedValue {
OwnedValue::Integer(result.0)
}
}
(OwnedValue::Float(lhs), OwnedValue::Float(rhs)) => {
OwnedValue::Float((lhs * rhs).round_to_precision(6.0))
}
(OwnedValue::Float(lhs), OwnedValue::Float(rhs)) => OwnedValue::Float(lhs * rhs),
(OwnedValue::Integer(i), OwnedValue::Float(f))
| (OwnedValue::Float(f), OwnedValue::Integer(i)) => OwnedValue::Float(*i as f64 * { *f }),
(OwnedValue::Null, _) | (_, OwnedValue::Null) => OwnedValue::Null,
@@ -1160,7 +1156,7 @@ mod tests {
),
(OwnedValue::Integer(0), OwnedValue::Text(Text::from_str(""))),
];
let outpus = [
let outputs = [
OwnedValue::Null,
OwnedValue::Integer(1),
OwnedValue::Null,
@@ -1174,13 +1170,13 @@ mod tests {
assert_eq!(
inputs.len(),
outpus.len(),
outputs.len(),
"Inputs and Outputs should have same size"
);
for (i, (lhs, rhs)) in inputs.iter().enumerate() {
assert_eq!(
exec_or(lhs, rhs),
outpus[i],
outputs[i],
"Wrong OR for lhs: {}, rhs: {}",
lhs,
rhs

View File

@@ -39,10 +39,12 @@ use crate::storage::wal::CheckpointResult;
use crate::storage::{btree::BTreeCursor, pager::Pager};
use crate::translate::plan::{ResultSetColumn, TableReference};
use crate::types::{
AggContext, Cursor, CursorResult, ExternalAggState, OwnedValue, OwnedValueType, Record,
SeekKey, SeekOp,
AggContext, Cursor, CursorResult, ExternalAggState, OwnedValue, Record, SeekKey, SeekOp,
};
use crate::util::{
cast_real_to_integer, cast_text_to_integer, cast_text_to_numeric, cast_text_to_real,
checked_cast_text_to_numeric, parse_schema_rows, RoundToPrecision,
};
use crate::util::{parse_schema_rows, RoundToPrecision};
use crate::vdbe::builder::CursorType;
use crate::vdbe::insn::Insn;
use crate::vector::{vector32, vector64, vector_distance_cos, vector_extract};
@@ -3463,7 +3465,7 @@ fn exec_round(reg: &OwnedValue, precision: Option<OwnedValue>) -> OwnedValue {
let reg = _to_float(reg);
let round = |reg: f64, f: f64| {
let precision = if f < 1.0 { 0.0 } else { f };
OwnedValue::Float(reg.round_to_precision(precision))
OwnedValue::Float(reg.round_to_precision(precision as i32))
};
match precision {
Some(OwnedValue::Text(x)) => match cast_text_to_numeric(x.as_str()) {
@@ -3644,132 +3646,6 @@ fn exec_replace(source: &OwnedValue, pattern: &OwnedValue, replacement: &OwnedVa
}
}
/// When casting a TEXT value to INTEGER, the longest possible prefix of the value that can be interpreted as an integer number
/// is extracted from the TEXT value and the remainder ignored. Any leading spaces in the TEXT value when converting from TEXT to INTEGER are ignored.
/// If there is no prefix that can be interpreted as an integer number, the result of the conversion is 0.
/// If the prefix integer is greater than +9223372036854775807 then the result of the cast is exactly +9223372036854775807.
/// Similarly, if the prefix integer is less than -9223372036854775808 then the result of the cast is exactly -9223372036854775808.
/// When casting to INTEGER, if the text looks like a floating point value with an exponent, the exponent will be ignored
/// because it is no part of the integer prefix. For example, "CAST('123e+5' AS INTEGER)" results in 123, not in 12300000.
/// The CAST operator understands decimal integers only — conversion of hexadecimal integers stops at the "x" in the "0x" prefix of the hexadecimal integer string and thus result of the CAST is always zero.
fn cast_text_to_integer(text: &str) -> OwnedValue {
let text = text.trim();
if text.is_empty() {
return OwnedValue::Integer(0);
}
if let Ok(i) = text.parse::<i64>() {
return OwnedValue::Integer(i);
}
let bytes = text.as_bytes();
let mut end = 0;
if bytes[0] == b'-' {
end = 1;
}
while end < bytes.len() && bytes[end].is_ascii_digit() {
end += 1;
}
text[..end]
.parse::<i64>()
.map_or(OwnedValue::Integer(0), OwnedValue::Integer)
}
/// When casting a TEXT value to REAL, the longest possible prefix of the value that can be interpreted
/// as a real number is extracted from the TEXT value and the remainder ignored. Any leading spaces in
/// the TEXT value are ignored when converging from TEXT to REAL.
/// If there is no prefix that can be interpreted as a real number, the result of the conversion is 0.0.
fn cast_text_to_real(text: &str) -> OwnedValue {
let trimmed = text.trim();
if trimmed.is_empty() {
return OwnedValue::Float(0.0);
}
if let Ok(num) = trimmed.parse::<f64>() {
return OwnedValue::Float(num);
}
let Ok((_, text)) = parse_numeric_str(trimmed) else {
return OwnedValue::Float(0.0);
};
text.parse::<f64>()
.map_or(OwnedValue::Float(0.0), OwnedValue::Float)
}
/// NUMERIC Casting a TEXT or BLOB value into NUMERIC yields either an INTEGER or a REAL result.
/// If the input text looks like an integer (there is no decimal point nor exponent) and the value
/// is small enough to fit in a 64-bit signed integer, then the result will be INTEGER.
/// Input text that looks like floating point (there is a decimal point and/or an exponent)
/// and the text describes a value that can be losslessly converted back and forth between IEEE 754
/// 64-bit float and a 51-bit signed integer, then the result is INTEGER. (In the previous sentence,
/// a 51-bit integer is specified since that is one bit less than the length of the mantissa of an
/// IEEE 754 64-bit float and thus provides a 1-bit of margin for the text-to-float conversion operation.)
/// Any text input that describes a value outside the range of a 64-bit signed integer yields a REAL result.
/// Casting a REAL or INTEGER value to NUMERIC is a no-op, even if a real value could be losslessly converted to an integer.
pub fn checked_cast_text_to_numeric(text: &str) -> std::result::Result<OwnedValue, ()> {
// sqlite will parse the first N digits of a string to numeric value, then determine
// whether _that_ value is more likely a real or integer value. e.g.
// '-100234-2344.23e14' evaluates to -100234 instead of -100234.0
let (kind, text) = parse_numeric_str(text)?;
match kind {
OwnedValueType::Integer => Ok(text
.parse::<i64>()
.map_or(OwnedValue::Integer(0), OwnedValue::Integer)),
OwnedValueType::Float => Ok(text
.parse::<f64>()
.map_or(OwnedValue::Float(0.0), OwnedValue::Float)),
_ => unreachable!(),
}
}
fn parse_numeric_str(text: &str) -> Result<(OwnedValueType, &str), ()> {
let bytes = text.trim_start().as_bytes();
let mut end = 0;
let mut has_decimal = false;
let mut has_exponent = false;
if bytes[0] == b'-' {
end = 1;
}
while end < bytes.len() {
match bytes[end] {
b'0'..=b'9' => end += 1,
b'.' if !has_decimal && !has_exponent => {
has_decimal = true;
end += 1;
}
b'e' | b'E' if !has_exponent => {
has_exponent = true;
end += 1;
// allow exponent sign
if end < bytes.len() && (bytes[end] == b'+' || bytes[end] == b'-') {
end += 1;
}
}
_ => break,
}
}
if end == 0 || (end == 1 && bytes[0] == b'-') {
return Err(());
}
Ok((
if !has_decimal && !has_exponent {
OwnedValueType::Integer
} else {
OwnedValueType::Float
},
&text[..end],
))
}
fn cast_text_to_numeric(txt: &str) -> OwnedValue {
checked_cast_text_to_numeric(txt).unwrap_or(OwnedValue::Integer(0))
}
// Check if float can be losslessly converted to 51-bit integer
fn cast_real_to_integer(float: f64) -> std::result::Result<i64, ()> {
let i = float as i64;
if float == i as f64 && i.abs() < (1i64 << 51) {
return Ok(i);
}
Err(())
}
fn execute_sqlite_version(version_integer: i64) -> String {
let major = version_integer / 1_000_000;
let minor = (version_integer % 1_000_000) / 1_000;

View File

@@ -340,8 +340,8 @@ do_execsql_test multiply-agg-float-agg-int {
} {41118.0}
do_execsql_test multiply-str-floats-edgecase {
SELECT '-123.22342-24' * '232.3x32';
} {-28624.800466}
SELECT '-123.22341-24' * '232.3x32';
} {-28624.798143}
do_execsql_test divide-agg-int {