From 3f2afd33454658421bf19d853e643ff623bea0fe Mon Sep 17 00:00:00 2001 From: Tam Nguyen Duc <1218621+tamnd@users.noreply.github.com> Date: Tue, 25 Aug 2026 16:56:31 +0700 Subject: [PATCH] Store the wide decimal on the sixteen byte plane A decimal is one declared type stored two ways. Up to eighteen digits the column is a lane word of unscaled units; above that it is sixteen little endian bytes of them at a fixed stride, which is the plane INT128 opened and the layout BINARY(16) already used. The precision in the declared type is what tells writer and reader which, and both ask the same function, fixed_octets, so they cannot disagree. That moves the frontier for the type to where an i128 ends. Thirty eight digits is the widest an i128 holds, so it is the widest a value of one can be, and DECIMAL(39,2) is refused at the declaration rather than at the table. A bare DECIMAL, which is DECIMAL(38,0), becomes a declarable and storable property type for the first time. DECIMAL(38,2) was the canonical spelled and not storable example in six places. All six now use INT256, which is durable: it will not become storable without a two hundred and fifty six bit value carrier. The directory version goes to 14. --- conformance/cases/error.yaml | 12 +- crates/zu-zu1/src/catalog.rs | 12 +- crates/zu-zu1/src/props.rs | 146 +++++++++++++----- crates/zu/src/declare.rs | 76 +++++++++ crates/zu/src/insert.rs | 19 ++- crates/zu/src/query.rs | 36 +++-- crates/zu/src/session.rs | 11 +- crates/zu/tests/catalog_statements.rs | 2 +- crates/zu/tests/graph_type.rs | 55 +++---- crates/zu/tests/refusal_shape.rs | 2 +- crates/zu/tests/refusals.rs | 2 +- .../tests/snapshots/declaration-refusals.txt | 2 +- 12 files changed, 277 insertions(+), 98 deletions(-) diff --git a/conformance/cases/error.yaml b/conformance/cases/error.yaml index 2fb954bf..b6bd779e 100644 --- a/conformance/cases/error.yaml +++ b/conformance/cases/error.yaml @@ -197,21 +197,21 @@ cases: raises: "42000" - name: a-property-type-no-column-can-hold - doc: A declared type is written into the catalog with the codes a column stores, so a type no column has is a type no element type can name. A decimal column holds unscaled units in a 64 bit word and the declared scale is what makes them a number, so thirty eight digits of them are more than a column here has room for. The refusal is about the statement rather than about the file, which is why it is 42000 and not a report of damage. - query: "CREATE GRAPH TYPE t { (:P {v :: DECIMAL(38,2)}) }" + doc: A declared type is written into the catalog with the codes a column stores, so a type no column has is a type no element type can name. A two hundred and fifty six bit integer is one of those, and the reason is the value side rather than the storage side: nothing here carries a number that wide, so a column of them could take a row in and be unable to give it back. The refusal is about the statement rather than about the file, which is why it is 42000 and not a report of damage. + query: "CREATE GRAPH TYPE t { (:P {v :: INT256}) }" raises: "42000" - name: a-decimal-column-is-a-declaration-a-graph-type-takes - doc: The other side of the line above. A decimal whose unscaled units fit a 64 bit word is a column, so the declaration stands, and eighteen digits is the widest that does. The declaration is the setup because a catalog statement returns no rows, so what the case asserts is that it did not raise. + doc: A decimal is a column at every precision a statement can spell. To eighteen digits its unscaled units ride a 64 bit lane word and above that they are sixteen bytes a row, which is one declared type stored two ways and is the declaration and encoding split at its plainest. A bare DECIMAL is DECIMAL(38,0), the widest of them, so this case is the far end of the range as well as the ordinary spelling of it. The declaration is the setup because a catalog statement returns no rows, so what the case asserts is that it did not raise. setup: - - "CREATE GRAPH TYPE ledger { (:Purchase {total :: DECIMAL(18,2)}) }" - query: RETURN 18 AS digits + - "CREATE GRAPH TYPE ledger { (:Purchase {total :: DECIMAL(18,2), paid :: DECIMAL(29,4), owed :: DECIMAL}) }" + query: RETURN 38 AS digits columns: - digits rows: - values: - type: INT64 - value: "18" + value: "38" - name: an-int128-column-is-a-declaration-a-graph-type-takes doc: A signed hundred and twenty eight bit integer is a column, stored as sixteen bytes a row at a fixed stride rather than in the sixty four bit lane every other number rides. It reads back as an exact numeric of scale nought, which is a carrier the engine has for the whole of that range. The declaration is the setup because a catalog statement returns no rows, so what the case asserts is that it did not raise. diff --git a/crates/zu-zu1/src/catalog.rs b/crates/zu-zu1/src/catalog.rs index 2bedd85d..bc83f1dd 100644 --- a/crates/zu-zu1/src/catalog.rs +++ b/crates/zu-zu1/src/catalog.rs @@ -2127,21 +2127,23 @@ mod tests { .expect("a list of lists is declarable"); // A type the declared form has no shape for at all is still // refused where it is written and not where it is encoded. A - // narrow decimal is no longer one of those, since its unscaled - // units ride a lane word, so the example is one whose units do - // not: thirty eight digits want more than sixty four bits. + // decimal is no longer one of those at any precision a statement + // can spell, since its unscaled units ride a lane word to + // eighteen digits and sixteen bytes to thirty eight, so the + // example is one past the end of both: thirty nine digits want + // more than the `i128` a value of one arrives in. let err = c .add_graph_type(GraphType::open("money").with( ElementType::node("Purchase", vec![person]).with_property( "total", LogicalType::Decimal { - precision: 38, + precision: 39, scale: 2, }, true, ), )) - .expect_err("a wide decimal has no declared form") + .expect_err("a decimal past the carrier has no declared form") .to_string(); assert!(err.contains("a type this file cannot write"), "{err}"); } diff --git a/crates/zu-zu1/src/props.rs b/crates/zu-zu1/src/props.rs index ab4c9579..ce0e5aca 100644 --- a/crates/zu-zu1/src/props.rs +++ b/crates/zu-zu1/src/props.rs @@ -80,7 +80,11 @@ use crate::txn::Cell; /// hundred and twenty eight bit integer, which is the first column type /// that is a number and does not ride the lane: sixteen bytes at a fixed /// stride, on the side of the store a `BINARY(16)` already uses. -const PROPS_VERSION: u16 = 13; +/// Version 14 puts the wide decimal on that same plane, which is the +/// first type here stored two ways: `DECIMAL(9,2)` is a lane word of +/// unscaled units and `DECIMAL(29,2)` is sixteen bytes of them, and the +/// precision in the declared type is what tells the reader which. +const PROPS_VERSION: u16 = 14; const MAX_NAME_LEN: usize = 256; /// The code a list column is written under, followed on disk by the @@ -484,16 +488,23 @@ fn extended_bytes(ty: &LogicalType) -> Option> { bits: IntBits::B128, precision: None, } => vec![EXTENDED_CODE, EXT_INT128], - // A decimal is written where its unscaled units fit the lane, - // and refused where they do not. The bound belongs here rather - // than one caller up because the declared form and the column - // form are one encoding for this type: a precision no column can - // hold is a precision no element type can name, and the refusal - // then lands at the declaration, where the user wrote it, rather - // than at the first insert. Wider precisions want a plane of - // their own the way a zoned column has one, and that is S2's. + // A decimal's unscaled units go wherever they fit: a lane word + // to eighteen digits, and the sixteen byte plane above that, + // which is the one the hundred and twenty eight bit integer + // opened. Which of the two a column uses is the encoding and is + // not written here; what is written here is the declaration, + // and the bound on it is the widest an `i128` holds, since that + // is the carrier a value of one arrives in. + // + // The bound belongs here rather than one caller up because the + // declared form and the column form are one encoding for this + // type: a precision no column can hold is a precision no element + // type can name, and the refusal then lands at the declaration, + // where the user wrote it, rather than at the first insert. LogicalType::Decimal { precision, scale } => { - lane_width(ty)?; + if *precision > zu_common::decimal::MAX_DIGITS { + return None; + } let mut out = vec![EXTENDED_CODE, EXT_DECIMAL]; out.extend(precision.to_le_bytes()); out.extend(scale.to_le_bytes()); @@ -1145,20 +1156,23 @@ impl PropValues<'_> { pub fn none_of(ty: &LogicalType) -> Option> { Some(match ty { LogicalType::Bool => PropValues::Bool(&[]), - // A decimal column is a lane of unscaled units, so what an - // empty one holds none of is integers. The scale that makes - // them a number is in the declared type beside them, which - // is why this can be the same empty lane an integer column - // gets without the two columns being the same column. - // And a hundred and twenty eight bit integer holds none of - // sixteen byte runs, because it is on the blob side. The two - // integer arms are two arms for the same reason a `BINARY` - // column and an `INT64` one are: what an empty column holds - // none of is whatever its rows would have been. + // A narrow decimal column is a lane of unscaled units, so + // what an empty one holds none of is integers. The scale + // that makes them a number is in the declared type beside + // them, which is why this can be the same empty lane an + // integer column gets without the two columns being the same + // column. A wide one holds none of sixteen byte runs, and so + // does a hundred and twenty eight bit integer, because both + // are on the blob side. The arms are arms for the same reason + // a `BINARY` column and an `INT64` one are: what an empty + // column holds none of is whatever its rows would have been, + // and for a decimal that is the precision's answer and not + // the family's. LogicalType::Int { bits: IntBits::B128, .. } => PropValues::Bytes(&[]), + LogicalType::Decimal { .. } if fixed_octets(ty).is_some() => PropValues::Bytes(&[]), LogicalType::Int { .. } | LogicalType::Decimal { .. } => PropValues::Int(&[]), LogicalType::Float { .. } => PropValues::Float(&[]), LogicalType::Str { .. } => PropValues::Str(&[]), @@ -1962,14 +1976,20 @@ fn write_props( /// fixes is a width in a unit the storage does not count in. A bound on /// characters is a check; a bound on octets is a layout. /// -/// A hundred and twenty eight bit integer is one of these, and it is the -/// only one that is not a byte string. The lane is sixty four bits and -/// this is twice that, so the number goes on the blob side of the store -/// as sixteen little endian bytes, which is where a `BINARY(16)` already -/// goes and by the same layout: one width, no offsets. What tells the -/// two apart afterwards is the column's own type, the way it tells a -/// byte string from a character string. -pub(crate) fn fixed_octets(ty: &LogicalType) -> Option { +/// Two of these are numbers rather than byte strings. The lane is sixty +/// four bits, so a hundred and twenty eight bit integer and a decimal +/// whose unscaled units want more than a word both go on the blob side +/// of the store as sixteen little endian bytes, which is where a +/// `BINARY(16)` already goes and by the same layout: one width, no +/// offsets. What tells the three apart afterwards is the column's own +/// type, the way it tells a byte string from a character string. +/// +/// The decimal is the one whose answer here depends on an argument +/// rather than on the type, and that is the declared and encoding split +/// at its plainest: `DECIMAL(9,2)` and `DECIMAL(29,2)` are one declared +/// type family stored two ways, and the reader is told which by the +/// precision it was declared with. +pub fn fixed_octets(ty: &LogicalType) -> Option { match ty { LogicalType::Bytes { min: Some(min), @@ -1980,6 +2000,7 @@ pub(crate) fn fixed_octets(ty: &LogicalType) -> Option { bits: IntBits::B128, .. } => Some(16), + LogicalType::Decimal { .. } if lane_width(ty).is_none() => Some(16), _ => None, } } @@ -2090,6 +2111,35 @@ fn check_declared( // type says is not there. The scale is not checked because the // lane cannot disagree with it: a unit is whatever the declared // scale says a unit is. + // A wide decimal is the same promise checked over the other + // plane. Its unscaled units are sixteen bytes a row rather than + // a lane word, so the width is checked first, for the reason the + // integer above checks one, and then the digit count, which is + // the declaration's own promise and is the same question either + // way. + (LogicalType::Decimal { precision, scale }, PropValues::Bytes(rows)) + if fixed_octets(ty).is_some() => + { + for (row, bytes) in rows.iter().enumerate() { + if !column.holds(row) { + continue; + } + let Ok(word) = <[u8; 16]>::try_from(*bytes) else { + return Err(ZuError::InvalidArgument(format!( + "column '{name}' is declared {ty} and row {row} holds {} octets", + bytes.len() + ))); + }; + let held = Decimal::new(i128::from_le_bytes(word), *scale); + if held.digits() <= *precision { + continue; + } + return Err(ZuError::InvalidArgument(format!( + "column '{name}' is declared {ty} and row {row} holds {held}" + ))); + } + return Ok(()); + } (LogicalType::Decimal { precision, scale }, PropValues::Int(words)) => { for (row, &word) in words.iter().enumerate() { if !column.holds(row) { @@ -6006,12 +6056,14 @@ mod tests { }; assert_eq!(column_type_bytes(&fixed), declared_type_bytes(&fixed)); // A decimal is the first type whose storability turns on an - // argument rather than on the type. It rides the lane as a whole - // number of unscaled units, so the question is whether the units - // fit a lane word, and eighteen digits is the last precision - // that does. Both forms answer alike, so a decimal a graph type - // names is a decimal a column holds and there is no gap between - // them for a declaration to fall into. + // argument rather than on the type, and now the first stored two + // ways. Its unscaled units ride a lane word to eighteen digits + // and sixteen bytes on the blob side above that, so where it + // stops is where an `i128` stops, which is where the value that + // carries one stops. Both forms answer alike at every precision, + // so a decimal a graph type names is a decimal a column holds + // and there is no gap between them for a declaration to fall + // into. let decimal = |precision| LogicalType::Decimal { precision, scale: 2, @@ -6020,8 +6072,15 @@ mod tests { let ty = decimal(precision); assert_eq!(column_type_bytes(&ty), declared_type_bytes(&ty), "{ty}"); assert!(storable(&ty), "{ty}"); + assert!(fixed_octets(&ty).is_none(), "{ty} is on the lane"); } for precision in [19, 38] { + let ty = decimal(precision); + assert_eq!(column_type_bytes(&ty), declared_type_bytes(&ty), "{ty}"); + assert!(storable(&ty), "{ty}"); + assert_eq!(fixed_octets(&ty), Some(16), "{ty}"); + } + for precision in [39, 76] { let ty = decimal(precision); assert!(declared_type_bytes(&ty).is_none(), "{ty}"); assert!(column_type_bytes(&ty).is_none(), "{ty}"); @@ -6229,10 +6288,12 @@ mod tests { ), (bounded_list(LogicalType::float(FloatBits::B32), 768), true), (list_of(list_of(LogicalType::string())), true), - // A decimal whose unscaled units fit the lane. This is the - // one row of the table whose sibling is on the other side of - // it: the type is storable and an argument to it is what - // decides, which no other row here can say. + // Both decimals, which are one type stored two ways: twelve + // digits of unscaled units are a lane word and thirty eight + // are sixteen bytes on the blob side. The row above the + // frontier and the row below it used to be these two, and + // now the frontier for this type is where an `i128` ends, + // which is where the value carrier ends. ( LogicalType::Decimal { precision: 12, @@ -6240,12 +6301,19 @@ mod tests { }, true, ), + ( + LogicalType::Decimal { + precision: 38, + scale: 2, + }, + true, + ), (LogicalType::int(IntBits::B128), true), // Declarable and not storable. Each of these is an entry in // schema/06 section 6, and S2 turns them true one at a time. ( LogicalType::Decimal { - precision: 38, + precision: 39, scale: 2, }, false, diff --git a/crates/zu/src/declare.rs b/crates/zu/src/declare.rs index dced3a42..e5b45a1a 100644 --- a/crates/zu/src/declare.rs +++ b/crates/zu/src/declare.rs @@ -1277,6 +1277,82 @@ mod tests { assert_eq!(err.gqlstatus().map(|s| s.code()), Some("22003")); } + /// A decimal wider than a lane word is the same column on the other + /// plane, and the declaration still says what a unit is. + /// + /// This is the first type here stored two ways. `DECIMAL(12,2)` + /// above is a lane word of unscaled units and `DECIMAL(30,4)` is + /// sixteen bytes of them, and nothing about the declaration says + /// which: the precision does, through `props::fixed_octets`, which + /// is the one place that decides so the writer and the reader cannot + /// disagree. A caller writing the statement sees one type. + /// + /// What the wide plane buys is the range an `i128` has, which is + /// where a decimal now stops, because that is the carrier a value of + /// one arrives in. Thirty eight digits is the widest a declaration + /// may ask for and the widest a value can be, so the two ends meet. + #[test] + fn a_declared_wide_decimal_keeps_its_scale_on_the_other_plane() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("wide-decimal.zu1"); + seeded(&path); + let mut session = Session::open(&path).expect("open"); + // Twenty six digits and four places, which is a number of units + // no lane word holds: the units alone are thirty digits. + let units = "1234567890123456789012345678.9012"; + for stmt in [ + "CREATE PROPERTY GRAPH TYPE t { (:tally {n :: DECIMAL(32,4)}) }".to_string(), + "CREATE GRAPH g TYPED t".to_string(), + format!("USE g INSERT (t:tally {{n: CAST('{units}' AS DECIMAL(32,4))}})"), + // At the column's scale, coarser than it, and a plain + // integer. All three are exact at four places. + "USE g INSERT (t:tally {n: CAST('0.5' AS DECIMAL(5,1))})".to_string(), + "USE g INSERT (t:tally {n: 7})".to_string(), + ] { + session + .run(&stmt, &[]) + .expect("the graph, its type, its rows"); + } + + // The scale comes from the column and not from the literal, the + // same way it does on the lane, so 0.5 reads back at four + // places. That is the declaration doing the work on a plane + // where the value is sixteen bytes rather than a word. + let rows = session + .run("USE g MATCH (t:tally) RETURN t.n AS n ORDER BY n", &[]) + .expect("the rows read back"); + let read: Vec = rows + .rows + .iter() + .map(|row| match &row[0] { + Value::Decimal(d) => d.to_string(), + other => panic!("expected an exact decimal, got {other:?}"), + }) + .collect(); + assert_eq!(read, ["0.5000", "7.0000", units]); + + // The declared precision still binds, and it binds on the wide + // plane the way it does on the narrow one: thirty three digits + // is not a value of a column declared with thirty two. + let err = session + .run( + "USE g INSERT (t:tally {n: CAST('12345678901234567890123456789.0123' AS DECIMAL(33,4))})", + &[], + ) + .expect_err("thirty three digits do not fit thirty two"); + assert_eq!(err.gqlstatus().map(|s| s.code()), Some("22003")); + + // And a fifth place is a place this column has no room for, + // which is the refusal the narrow decimal already made. + let err = session + .run( + "USE g INSERT (t:tally {n: CAST('1.00001' AS DECIMAL(6,5))})", + &[], + ) + .expect_err("the column has four places and this has five"); + assert_eq!(err.gqlstatus().map(|s| s.code()), Some("22003")); + } + /// An `INT128` column holds numbers no lane word could, and hands /// them back whole. /// diff --git a/crates/zu/src/insert.rs b/crates/zu/src/insert.rs index 20cf4c5d..1863af26 100644 --- a/crates/zu/src/insert.rs +++ b/crates/zu/src/insert.rs @@ -24,6 +24,7 @@ use std::sync::Arc; use zu_common::gqlstatus::{Subject, codes}; use zu_common::{Decimal, FloatBits, GqlStatus, IntBits, LogicalType, Result, Temporal, ZuError}; use zu_query::binder::{BoundExpr, BoundInsertNode, BoundInsertRel}; +use zu_zu1::props::fixed_octets; use crate::deleted::Deleted; use crate::query::Value; @@ -760,18 +761,32 @@ pub(crate) fn cell(ty: &LogicalType, value: &Value, key: &str) -> Result { // // An integer is an exact number of scale nought, so it goes in // the same way a whole number goes into a float column. + // + // Where the units go is the precision's answer. To eighteen + // digits they are a lane word; above that they are sixteen + // bytes on the blob side, the plane the hundred and twenty + // eight bit integer opened, and `fixed_octets` is the one place + // that says which so the writer and the reader cannot disagree. + // The check either way is the declared precision, which is the + // promise, and not the width, which is the consequence. (LogicalType::Decimal { precision, scale }, Value::Decimal(_) | Value::Int(_)) => { let given = match value { Value::Decimal(d) => *d, Value::Int(n) => Decimal::new(i128::from(*n), 0), _ => unreachable!("matched just above"), }; + let wide = fixed_octets(ty).is_some(); let at = given .rescale(*scale) .filter(|at| at.digits() <= *precision) - .and_then(|at| i64::try_from(at.unscaled()).ok()); + .and_then(|at| match wide { + true => Some(Cell::Str(at.unscaled().to_le_bytes().to_vec())), + false => i64::try_from(at.unscaled()) + .ok() + .map(|n| Cell::Int(n as u64)), + }); match at { - Some(units) => Cell::Int(units as u64), + Some(cell) => cell, None => { return Err(ZuError::gql( codes::C22003, diff --git a/crates/zu/src/query.rs b/crates/zu/src/query.rs index 545f7197..1cdb5de9 100644 --- a/crates/zu/src/query.rs +++ b/crates/zu/src/query.rs @@ -662,27 +662,41 @@ fn column_value( reader.read_str(db, col, row, &mut bytes)?; Ok(Value::Bytes(bytes)) } - // A hundred and twenty eight bit integer is the same blob read - // as a number: sixteen little endian bytes, because the lane is - // sixty four bits and this is twice that. + // The two numbers the lane has no room for are the same blob + // read as a number: sixteen little endian bytes, because the + // lane is sixty four bits and these are twice that. // - // It comes back as an exact numeric of scale nought rather than - // as `Value::Int`, which holds sixty four bits and would lose - // the half of the column that the type is for. An exact numeric - // at scale nought is an integer, prints as one and compares - // equal to one, so `p.id = 5` is true of a row holding five; - // what it also is, is a hundred and twenty eight bit one. + // A hundred and twenty eight bit integer comes back as an exact + // numeric of scale nought rather than as `Value::Int`, which + // holds sixty four bits and would lose the half of the column + // that the type is for. An exact numeric at scale nought is an + // integer, prints as one and compares equal to one, so `p.id = + // 5` is true of a row holding five; what it also is, is a + // hundred and twenty eight bit one. + // + // A decimal reaching here is a wide one, since a narrow one is + // on the lane and returned above. The bytes are its unscaled + // units and the declared scale is what makes them a number, + // which is the same reading the lane arm does over a word. LogicalType::Int { bits: IntBits::B128, .. - } => { + } + | LogicalType::Decimal { .. } => { + let scale = match ty { + LogicalType::Decimal { scale, .. } => scale, + _ => 0, + }; let mut bytes = Vec::new(); reader.read_str(db, col, row, &mut bytes)?; let word: [u8; 16] = bytes.as_slice().try_into().map_err(|_| ZuError::Corrupt { what: "props column", detail: format!("'{key}' row {row} is {} octets and not 16", bytes.len()), })?; - Ok(Value::Decimal(Decimal::new(i128::from_le_bytes(word), 0))) + Ok(Value::Decimal(Decimal::new( + i128::from_le_bytes(word), + scale, + ))) } // A stored list comes back as the list value the rest of the // engine already has, element by element through the same diff --git a/crates/zu/src/session.rs b/crates/zu/src/session.rs index e65f75f1..163447cc 100644 --- a/crates/zu/src/session.rs +++ b/crates/zu/src/session.rs @@ -4266,13 +4266,14 @@ mod tests { let epoch = session.epoch; let labels = session.graph.catalog().labels().len(); // The catalog writes a property type in a declared form, and a - // decimal this wide has no shape in it: its unscaled units want - // more than the sixty four bit lane word a narrow one rides. - // The statement interned `Ghost` on the way to finding that out, - // and the file still has to come out unchanged. + // two hundred and fifty six bit integer has no shape in it: + // nothing here carries a value of one, so a column of them could + // take a row in and not give it back. The statement interned + // `Ghost` on the way to finding that out, and the file still has + // to come out unchanged. let err = session .run( - "CREATE GRAPH TYPE strict { (:Ghost {seen :: DECIMAL(38,2)}) }", + "CREATE GRAPH TYPE strict { (:Ghost {seen :: INT256}) }", &[], ) .expect_err("a property type no column can hold") diff --git a/crates/zu/tests/catalog_statements.rs b/crates/zu/tests/catalog_statements.rs index 383b1e70..5bd8ab72 100644 --- a/crates/zu/tests/catalog_statements.rs +++ b/crates/zu/tests/catalog_statements.rs @@ -234,7 +234,7 @@ fn every_element_type_a_graph_type_may_declare_is_performed() { /// A union of two property types is a `value_type` the grammar allows /// and the parser reads, and there is no column that holds either an /// `INT64` or a `STRING`, so the catalog will not write it. The refusal -/// is the same shape `graph_type.rs` pins for `DECIMAL(38,2)`: 42000, +/// is the same shape `graph_type.rs` pins for `INT256`: 42000, /// and nothing about a file that is not damaged. S2 decides whether a /// union gets a column at all. #[test] diff --git a/crates/zu/tests/graph_type.rs b/crates/zu/tests/graph_type.rs index add1016e..23f815f8 100644 --- a/crates/zu/tests/graph_type.rs +++ b/crates/zu/tests/graph_type.rs @@ -96,11 +96,15 @@ fn probe() -> Vec<(&'static str, Answer)> { // so a declaration is still all they are. ("LIST[768]", Answer::Declared), ("LIST>", Answer::Declared), - // A decimal a 64 bit lane holds. The column stores unscaled - // units and the declared scale is what makes them a number, so - // twelve digits of them fit a word and this is a column a - // statement can fill. + // The two decimals, which are one declared type stored two ways. + // The column stores unscaled units and the declared scale is + // what makes them a number; twelve digits of them fit a lane + // word and thirty eight want the sixteen byte plane INT128 + // below opened. Both are columns a statement can fill, and + // thirty eight is the widest an i128 holds, so it is the widest + // a value of one can be. ("DECIMAL(12,2)", Answer::Declared), + ("DECIMAL(38,2)", Answer::Declared), // The signed hundred and twenty eight bit integer is the first // number here that does not ride the lane. It is sixteen bytes // at a fixed stride, the layout BINARY(16) above already uses, @@ -111,13 +115,11 @@ fn probe() -> Vec<(&'static str, Answer)> { // The condition is 42000 for all of them, which is what S1 // replaced the sentence about corruption with. // - // The wide decimal's unscaled units want more than a lane word - // and the lane is sixty four bits. The two wide integers below - // it are refused for a different reason: the top half of a - // `UINT128` and the whole of an `INT256` are ranges no value - // the engine carries can name, so a column of one would take a - // row in and be unable to give it back. - ("DECIMAL(38,2)", cannot_write(28)), + // The two wide integers are refused on the value side rather + // than the storage side: sixteen bytes would hold either, but + // the top half of a `UINT128` and the whole of an `INT256` are + // ranges no value the engine carries can name, so a column of + // one would take a row in and be unable to give it back. ("INT256", cannot_write(29)), ("UINT128", cannot_write(30)), ("FLOAT16", cannot_write(31)), @@ -152,7 +154,7 @@ fn a_graph_type_declares_the_types_the_frontier_says_it_can() { declared += 1; } } - assert_eq!(declared, 27, "the declarable set changed"); + assert_eq!(declared, 28, "the declarable set changed"); } /// The frontier has a far side, and one type is on it. @@ -193,28 +195,29 @@ fn the_year_month_duration_is_stored_and_cannot_be_declared() { /// Every refusal above carries a condition and none of them says /// corrupt, which is what S1 changed. /// -/// A user who writes `DECIMAL(38,2)` has written a legal GQL statement -/// that this engine will not perform. Before S1 they were told their -/// file was damaged, with no condition to catch on. Now it is 42000, -/// syntax error or access rule violation, which is the class the -/// standard keeps for a statement the engine will not carry out. The -/// two data exception codes that say invalid value type, 22G03 and -/// 22G12, are class 22 and are about a value at run time; a type in a -/// declaration is not a value. +/// A user who writes `INT256` has written a legal GQL statement that +/// this engine will not perform. Before S1 they were told their file was +/// damaged, with no condition to catch on. Now it is 42000, syntax error +/// or access rule violation, which is the class the standard keeps for a +/// statement the engine will not carry out. The two data exception codes +/// that say invalid value type, 22G03 and 22G12, are class 22 and are +/// about a value at run time; a type in a declaration is not a value. /// -/// The example is the wide decimal rather than the narrow one because -/// the narrow one now stores. Thirty eight digits of unscaled units do -/// not fit a lane word, so this is where the same sentence still holds. +/// The example used to be the wide decimal and is now the wide integer, +/// because the decimal stores at every precision a statement can spell. +/// What is left on this side of the line is the types nothing here +/// carries a value of, and a two hundred and fifty six bit integer is +/// one: a column of them could take a row in and not give it back. #[test] fn a_type_the_catalog_will_not_write_is_refused_with_a_condition() { let dir = tempfile::tempdir().unwrap(); let mut db = graph(dir.path()); - let source = "CREATE GRAPH TYPE money { (:Purchase {total :: DECIMAL(38,2)}) }"; - let err = run(source, &mut db, &[]).expect_err("a wide decimal is not storable"); + let source = "CREATE GRAPH TYPE money { (:Purchase {total :: INT256}) }"; + let err = run(source, &mut db, &[]).expect_err("a wide integer is not storable"); assert_eq!(err.gqlstatus().map(|s| s.to_string()), Some("42000".into())); assert!(!err.to_string().contains("corrupt"), "{err}"); assert!( - err.to_string().contains("DECIMAL") || err.to_string().contains("total"), + err.to_string().contains("INT256") || err.to_string().contains("total"), "{err}" ); } diff --git a/crates/zu/tests/refusal_shape.rs b/crates/zu/tests/refusal_shape.rs index 94b9bdd2..4644d57a 100644 --- a/crates/zu/tests/refusal_shape.rs +++ b/crates/zu/tests/refusal_shape.rs @@ -70,7 +70,7 @@ const REFUSALS: &[(&str, &str)] = &[ ), ( "a type this file cannot write", - "CREATE GRAPH TYPE t6 { (:Probe {total :: DECIMAL(38,2)}) }", + "CREATE GRAPH TYPE t6 { (:Probe {total :: INT256}) }", ), ( "one property declared twice", diff --git a/crates/zu/tests/refusals.rs b/crates/zu/tests/refusals.rs index 2b4f09d4..3efbae94 100644 --- a/crates/zu/tests/refusals.rs +++ b/crates/zu/tests/refusals.rs @@ -70,7 +70,7 @@ fn refusals() -> Vec { Refusal { asks: "a type no column holds", setup: &[], - source: "CREATE GRAPH TYPE v1 { (:P {v :: DECIMAL(38,2)}) }", + source: "CREATE GRAPH TYPE v1 { (:P {v :: INT256}) }", says_corrupt: false, status: Some("42000"), }, diff --git a/crates/zu/tests/snapshots/declaration-refusals.txt b/crates/zu/tests/snapshots/declaration-refusals.txt index 744f448e..943857fb 100644 --- a/crates/zu/tests/snapshots/declaration-refusals.txt +++ b/crates/zu/tests/snapshots/declaration-refusals.txt @@ -49,7 +49,7 @@ ^ # a type this file cannot write - statement CREATE GRAPH TYPE t6 { (:Probe {total :: DECIMAL(38,2)}) } + statement CREATE GRAPH TYPE t6 { (:Probe {total :: INT256}) } gqlstatus 42000 condition syntax error or access rule violation severity X