Estado: ✅ Aceptada Fecha: 2026-05-29 Bloque: Y6 (quinto sub-bloque post-Y, el último item grande de tipos) Bump on-disk: 21 → 22

🧭 Contexto

Y mapeó DECIMAL/NUMERIC como alias puros de FLOAT — lo que ayudaba a portar schemas sin editar pero perdía precisión silenciosamente. Una columna amount DECIMAL(10,2) para guardar dinero almacenaba 0.1 + 0.2 = 0.30000000000000004 en lugar de 0.30 exacto. Inaceptable.

Y6 cierra esa brecha de la única forma correcta: nuevo tipo en disco, nueva variante de Value, encoding propio que preserva precisión arbitraria hasta 38 dígitos decimales.

💡 Decisión

1. Value::Decimal { value: i128, scale: u8 }

Variante nueva. value es la mantissa (entero sin decimales), scale indica cuántas posiciones del punto decimal hacia la derecha. Ejemplos:

i128 da ~38 dígitos significativos — suficiente para cualquier DECIMAL(p,s) con p ≤ 38.

2. ColumnType::Decimal (code=11)

DECIMAL, NUMERIC y DEC (con o sin (p,s)) ahora mapean a este tipo — ya no son aliases de FLOAT. Default cuando no hay sufijo: (10, 0) (convención PG/MySQL).

REAL, DOUBLE, DOUBLE PRECISION siguen siendo Float — para el caso en que sí querés un f64 nativo.

3. Por-columna: decimal_meta: Option<(u8, u8)> = (precision, scale)

Persiste en el catálogo con un nuevo flag bit COLUMN_FLAG_HAS_DECIMAL_META = 0x20 + 2 bytes después de int_width. Solo para Decimal columns. Precision 1..=38, scale 0..=precision.

4. Disk format por-fila

[present:u8=1][value:i128 LE = 16 bytes][scale:u8]

17 bytes por valor non-NULL (vs 8 para INT/FLOAT). Más caro en storage pero la precisión vale la pena.

5. Helpers exportados

pub fn decimal_to_string(value: i128, scale: u8) -> String;
pub(crate) fn parse_decimal(input: &str, target_scale: u8) -> Result<i128, String>;
pub(crate) fn parse_decimal_with_inferred_scale(input: &str) -> Result<(i128, u8), String>;
pub(crate) fn decimal_fits_precision(value: i128, scale: u8, precision: u8) -> bool;
pub(crate) fn value_to_decimal(v: &Value, target_scale: u8) -> Result<i128, String>;
pub(crate) fn rescale_decimal(value: i128, from: u8, to: u8) -> Result<i128, String>;
pub(crate) fn decimal_to_f64(value: i128, scale: u8) -> f64;
pub(crate) fn extract_decimal_meta(type_name: &str) -> Option<(u8, u8)>;

6. Comportamiento en INSERT/UPDATE

7. Comportamiento en lectura

El decoder restaura value: i128 + scale: u8 exactos. Value::Decimal { value: 12345, scale: 2 } se imprime como "123.45" en text contexts y se serializa como "123.45" (string en JSON, para no perder precisión en clientes que usan f64).

8. Aritmética con otros tipos

Decimal + Int, Decimal + Float, etc.: promueven a f64 (lossy). Documentado. Aritmética puramente Decimal-Decimal sería el siguiente paso (overflow check + rescale) pero queda diferida — el use case dominante es storage exact.

9. CAST AS DECIMAL

CAST(x AS DECIMAL) sin sufijo paramétrico infiere el scale del input (cuenta dígitos tras el .). Para precisión declarativa, recomendamos declarar la columna DECIMAL(p,s) y dejar que el encoder maneje el rescale.

10. Bump 21 → 22

Necesario por el code 11 en disco y el nuevo flag bit. V21 rechazado con [GBY-1003] — un V22 puede tener columnas DECIMAL que V21 no sabe decodificar.

📐 Códigos de error

Código Nombre Cuándo
GBY-4123 DECIMAL_OUT_OF_RANGE Parte entera de un valor DECIMAL(p,s) excede 10^(p-s), o overflow de i128.

🚫 Limitaciones documentadas

🔄 Cambios incompatibles

Tests y_* que asumían DECIMAL/NUMERICValue::Float fueron actualizados:

🧪 Validación

Suite y6_* en tests/integration_test.rs (13 tests):

Suite total: 580/580 pass (cargo test --lib --tests).

🔭 Futuro