razeri-plugin-contract 0.14.0

The dialect contract Razeri plugins are written against — rendering traits, data types, platform identity
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
// Copyright (C) 2026 Gabriel Shea
// Licensed under either of the Apache License, Version 2.0 or the MIT
// license, at your option.

//! **A value written as text reads the same on every engine.**
//!
//! Razeri writes values as text in two places a reader compares across
//! engines: a load that refuses its rows names the values it refused, and a
//! reconciliation writes each measurement as text so it can be compared with
//! the source's by string equality. An engine's plain cast to text is not
//! enough for either. Some engines write a date by a format the session sets,
//! some drop a decimal's trailing zeros, some write every digit of a
//! timestamp's declared precision, some write a zoned moment in the session's
//! zone, a floating ten as `10.0` or a truth value as `1`. [`CanonicalText`] says what the text is for
//! each type, and [`ExpressionRenderer::render_value_text`] is how an engine
//! writes it.
//!
//! [`ExpressionRenderer::render_value_text`]: crate::ExpressionRenderer::render_value_text

use crate::DataType;
use crate::data_type::FloatPrecision;

/// **What a value's text is, by its type.**
///
/// One classification, so no two engines can disagree about which types have
/// a canonical text and which are written by the engine's own cast.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CanonicalText {
    /// A calendar day: `YYYY-MM-DD`, as in `2026-01-05`.
    Date,
    /// A moment with no time zone: `YYYY-MM-DD HH:MM:SS`, then a point and the
    /// fraction of a second to the microsecond with its trailing zeros
    /// dropped. A whole second has no point and no fraction:
    /// `2026-01-05 12:34:56.12` and `2026-01-05 00:00:00`.
    Timestamp,
    /// A moment with a time zone: the same moment in UTC, written as a
    /// [`Timestamp`](Self::Timestamp) is and then `+00:00`, whatever zone the
    /// value or the session is in. `14:34:56.12` at `+02:00` is
    /// `2026-01-05 12:34:56.12+00:00`.
    ZonedTimestamp,
    /// A decimal: every digit of its declared scale after a point, and none
    /// where the scale is zero. A `DECIMAL(12, 2)` ten is `10.00`, a negative
    /// half is `-0.50`, and the separator is always a point.
    Decimal {
        /// How many digits follow the point.
        scale: u8,
    },
    /// A floating number: the value rounded to `significant` digits and
    /// written the way C's `printf` writes `%.{significant}g`.
    ///
    /// Without an exponent where the rounded value's power of ten is at
    /// least -4 and below `significant`, and with one otherwise; trailing
    /// zeros and a bare point dropped; an exponent as `e`, a sign and at least
    /// two digits. A double ten is `10` (never `10.0`), a tenth `0.1`,
    /// `0.1 + 0.2` is `0.3`, `1.5e-7` is `1.5e-07`, and `10^20` is `1e+20`. A
    /// zero is `0`, whatever its sign.
    ///
    /// A double carries [`DOUBLE_DIGITS`] and a single-precision number
    /// [`SINGLE_DIGITS`]: the most digits of a decimal that survive the trip
    /// into the type and back. A value the source wrote with that many digits
    /// or fewer reads back as written, and the binary noise past them — the
    /// `4` at the end of `0.30000000000000004` — is never shown. A
    /// single-precision value reads the same on an engine that holds it as a
    /// double. Not-a-number and the infinities have no canonical text.
    Float {
        /// How many significant digits the value is rounded to.
        significant: u8,
    },
    /// A truth value: `true` or `false`, on an engine that holds it as a
    /// number too.
    Boolean,
    /// Every other type, written by the engine's own cast to text.
    AsCast,
}

/// The significant digits a double is written with: the most a decimal keeps
/// through a double and back (C's `DBL_DIG`).
pub const DOUBLE_DIGITS: u8 = 15;

/// The significant digits a single-precision number is written with: the most
/// a decimal keeps through one and back (C's `FLT_DIG`).
pub const SINGLE_DIGITS: u8 = 6;

/// What a zoned moment's text ends with: its offset from UTC, which is none.
pub const UTC_OFFSET: &str = "+00:00";

/// The function an engine with no `printf` writes a double's text through:
/// fifteen significant digits, as `%.15g` writes them.
pub const FLOAT_TEXT_FUNCTION: &str = "razeri_float_text";

/// The function an engine with no `printf` writes a single-precision number's
/// text through: six significant digits, as `%.6g` writes them.
pub const SINGLE_FLOAT_TEXT_FUNCTION: &str = "razeri_single_float_text";

/// **Which function writes a floating number rounded to `significant`
/// digits**: [`SINGLE_FLOAT_TEXT_FUNCTION`] for [`SINGLE_DIGITS`], and
/// [`FLOAT_TEXT_FUNCTION`] for every other count.
#[must_use]
pub const fn float_text_function(significant: u8) -> &'static str {
    if significant == SINGLE_DIGITS { SINGLE_FLOAT_TEXT_FUNCTION } else { FLOAT_TEXT_FUNCTION }
}

/// **The functions an engine writes a value's text through**, as
/// [`ExpressionRenderer::value_text_functions`] answers them.
///
/// [`ExpressionRenderer::value_text_functions`]: crate::ExpressionRenderer::value_text_functions
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueTextFunctions {
    /// The schema they are created in, which a script makes first where it is
    /// not there yet; `None` where they are created wherever the script runs.
    pub schema: Option<&'static str>,
    /// The statements that create them, in order, each complete and without
    /// its terminator. Each can run again: it replaces what an earlier run
    /// made.
    pub statements: Vec<String>,
}

/// **An expression laid out one `WHEN` to a line**, so a function body built
/// from [`computed_float_text`] reads in a terminal and reaches the engine
/// through a client that misreads a very long line.
///
/// Only for an expression whose text literals hold no space, which is every
/// expression [`computed_float_text`] writes: a `WHEN` inside a literal would
/// be broken too.
#[must_use]
pub fn in_short_lines(expression: &str, indent: &str) -> String {
    expression.replace(" WHEN ", &format!("\n{indent}WHEN "))
}

impl CanonicalText {
    /// The canonical text of a value of `data_type`.
    #[must_use]
    pub fn of(data_type: &DataType) -> Self {
        match data_type {
            DataType::Date(_) => Self::Date,
            DataType::Timestamp(_) => Self::Timestamp,
            DataType::DateTime(moment) if !moment.with_timezone => Self::Timestamp,
            DataType::TimestampTz(_) | DataType::DateTime(_) => Self::ZonedTimestamp,
            DataType::Decimal(decimal) => Self::Decimal { scale: decimal.scale },
            DataType::Float(float) => Self::Float {
                significant: match float.precision {
                    FloatPrecision::Single => SINGLE_DIGITS,
                    FloatPrecision::Double => DOUBLE_DIGITS,
                },
            },
            DataType::Boolean(_) => Self::Boolean,
            _ => Self::AsCast,
        }
    }
}

/// **A truth value as `true` or `false`**, on an engine that takes the value
/// itself as a condition.
///
/// `NULL` stays `NULL`: neither branch is taken.
#[must_use]
pub fn truth_text(value: &str) -> String {
    format!("CASE WHEN {value} THEN 'true' WHEN NOT {value} THEN 'false' END")
}

/// **The pieces of a floating number's text** an engine computes for
/// [`float_text`], each an expression over the value's magnitude.
#[derive(Debug, Clone, Copy)]
pub struct FloatParts<'a> {
    /// The rounded magnitude's significant digits as text, exactly as many as
    /// are asked for, the first non-zero: `150000000000000` for `1.5e-7`.
    pub digits: &'a str,
    /// The rounded magnitude's power of ten, as an integer: `-7` for `1.5e-7`.
    pub exponent: &'a str,
    /// That power's size as text, without a sign or padding: `7` for
    /// `1.5e-7`, `100` for `1e100`.
    pub exponent_digits: &'a str,
}

/// **How an engine spells the text functions [`float_text`] uses.** Each
/// closure is handed the SQL of its operand.
pub struct TextFunctions<'a> {
    /// The engine's name for the three-argument `SUBSTR`.
    pub substring: &'a str,
    /// A text that holds a point with its trailing zeros, and then a point
    /// left bare, removed: [`rtrim_fraction`] where `RTRIM` takes the
    /// characters to remove.
    pub trim_fraction: &'a dyn Fn(&str) -> String,
    /// Text joined, in the engine's spelling.
    pub concat: &'a dyn Fn(&[&str]) -> String,
}

/// **A floating number's magnitude laid out as `%.{significant}g` lays it
/// out**, from the pieces an engine computed.
///
/// For an engine with no `printf`. The digits are placed around a point —
/// after the first, for an exponent's form; after the whole part; or after a
/// zero and the zeros a small value opens with — and the trailing zeros, then
/// a bare point, are trimmed. Uses `CASE` and `ABS` besides the functions it
/// is handed. The sign, a zero and a `NULL` are [`signed_float_text`]'s.
#[must_use]
pub fn float_text(parts: &FloatParts<'_>, significant: u8, text: &TextFunctions<'_>) -> String {
    let FloatParts { digits, exponent, exponent_digits } = *parts;
    let TextFunctions { substring, trim_fraction, concat } = *text;
    let mantissa = trim_fraction(&concat(&[
        &format!("{substring}({digits}, 1, 1)"),
        "'.'",
        &format!("{substring}({digits}, 2, {significant})"),
    ]));
    let scientific = concat(&[
        &mantissa,
        "'e'",
        &format!("CASE WHEN {exponent} < 0 THEN '-' ELSE '+' END"),
        &format!("CASE WHEN ABS({exponent}) < 10 THEN '0' ELSE '' END"),
        exponent_digits,
    ]);
    let below_one =
        trim_fraction(&concat(&[&format!("{substring}('0.000', 1, 1 - {exponent})"), digits]));
    let with_point = trim_fraction(&concat(&[
        &format!("{substring}({digits}, 1, {exponent} + 1)"),
        "'.'",
        &format!("{substring}({digits}, {exponent} + 2, {significant})"),
    ]));
    format!(
        "CASE WHEN {exponent} < -4 OR {exponent} >= {significant} THEN {scientific} \
         WHEN {exponent} < 0 THEN {below_one} ELSE {with_point} END"
    )
}

/// **How an engine with no `printf` spells the arithmetic
/// [`computed_float_text`] does.** Each closure is handed the SQL of its
/// operand.
pub struct FloatArithmetic<'a> {
    /// The base-ten logarithm of a positive number, rounded down to a whole
    /// number: `CAST(FLOOR(LOG10(x)) AS INTEGER)`.
    pub floor_log10: &'a dyn Fn(&str) -> String,
    /// Ten to a whole power, as a floating number: `POWER(10, x)`.
    pub power_of_ten: &'a dyn Fn(&str) -> String,
    /// A whole number of up to nineteen digits as text, with no point:
    /// `CAST(CAST(x AS DECIMAL(19, 0)) AS VARCHAR(19))`.
    pub whole_text: &'a dyn Fn(&str) -> String,
    /// The text functions the layout uses.
    pub text: TextFunctions<'a>,
}

/// **A floating number as `%.{significant}g` writes it, computed** on an
/// engine with no `printf`.
///
/// The power of ten is the floor of the magnitude's base-ten logarithm, less
/// one where the logarithm rounded up past a magnitude just under a power of
/// ten. The significant digits are the magnitude divided by that power and
/// scaled to a whole number of `significant` digits, rounded; a rounding that
/// carries into one more digit, as `9.999…` does, adds one to the power, and
/// the extra digit, a zero, is never read. [`float_text`] lays the pieces out
/// and [`signed_float_text`] adds the sign. A zero never reaches the
/// logarithm.
///
/// Uses `NULLIF`, `ABS`, `ROUND` with two arguments and `CASE` besides the
/// spellings it is handed.
#[must_use]
pub fn computed_float_text(value: &str, significant: u8, spelling: &FloatArithmetic<'_>) -> String {
    let magnitude = format!("NULLIF(ABS({value}), 0)");
    let floor = (spelling.floor_log10)(&magnitude);
    let power = format!(
        "({floor} - CASE WHEN {magnitude} < {} THEN 1 ELSE 0 END)",
        (spelling.power_of_ten)(&floor)
    );
    let scale = 10_u64.pow(u32::from(significant.saturating_sub(1)));
    let whole = format!("ROUND({magnitude} / {} * {scale}, 0)", (spelling.power_of_ten)(&power));
    let digits = (spelling.whole_text)(&whole);
    let exponent = format!("({power} + CASE WHEN {whole} >= {} THEN 1 ELSE 0 END)", scale * 10);
    let exponent_digits = (spelling.whole_text)(&format!("ABS({exponent})"));
    let magnitude_text = float_text(
        &FloatParts { digits: &digits, exponent: &exponent, exponent_digits: &exponent_digits },
        significant,
        &spelling.text,
    );
    signed_float_text(value, &magnitude_text, spelling.text.concat)
}

/// **A floating number's text from its magnitude's**: `NULL` for `NULL`, `0`
/// for a zero of either sign, and a minus sign before a negative value's
/// magnitude.
#[must_use]
pub fn signed_float_text(
    value: &str,
    magnitude: &str,
    concat: &dyn Fn(&[&str]) -> String,
) -> String {
    format!(
        "CASE WHEN {value} IS NULL THEN NULL WHEN {value} = 0 THEN '0' ELSE {} END",
        concat(&[&format!("CASE WHEN {value} < 0 THEN '-' ELSE '' END"), magnitude])
    )
}

/// **A timestamp written with six digits of fraction, with the trailing zeros
/// and then a bare point removed**, on an engine whose `RTRIM` takes the
/// characters to remove.
///
/// `written` must always carry a point and six digits after the seconds, as
/// `YYYY-MM-DD HH24:MI:SS.FF6` does. The point is what stops the first trim:
/// without it, a timestamp at `12:34:50` would lose the zero of its seconds.
#[must_use]
pub fn rtrim_fraction(written: &str) -> String {
    format!("RTRIM(RTRIM({written}, '0'), '.')")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::data_type::DateTimeDataType;

    #[test]
    fn a_date_a_moment_and_a_decimal_have_a_canonical_text() {
        assert_eq!(CanonicalText::of(&DataType::date()), CanonicalText::Date);
        assert_eq!(CanonicalText::of(&DataType::timestamp()), CanonicalText::Timestamp);
        assert_eq!(
            CanonicalText::of(&DataType::DateTime(DateTimeDataType::new(6, false))),
            CanonicalText::Timestamp
        );
        assert_eq!(
            CanonicalText::of(&DataType::decimal(12, 2)),
            CanonicalText::Decimal { scale: 2 }
        );
    }

    #[test]
    fn a_zoned_moment_a_float_and_a_truth_value_have_a_canonical_text() {
        assert_eq!(CanonicalText::of(&DataType::timestamp_tz()), CanonicalText::ZonedTimestamp);
        assert_eq!(
            CanonicalText::of(&DataType::DateTime(DateTimeDataType::new(6, true))),
            CanonicalText::ZonedTimestamp
        );
        assert_eq!(
            CanonicalText::of(&DataType::double()),
            CanonicalText::Float { significant: 15 }
        );
        assert_eq!(CanonicalText::of(&DataType::float()), CanonicalText::Float { significant: 6 });
        assert_eq!(CanonicalText::of(&DataType::boolean()), CanonicalText::Boolean);
    }

    #[test]
    fn text_and_whole_numbers_are_written_by_the_cast() {
        for data_type in [DataType::varchar(10), DataType::bigint(), DataType::text()] {
            assert_eq!(CanonicalText::of(&data_type), CanonicalText::AsCast, "{data_type:?}");
        }
    }

    #[test]
    fn a_truth_value_is_named_by_a_condition() {
        assert_eq!(truth_text("b"), "CASE WHEN b THEN 'true' WHEN NOT b THEN 'false' END");
    }

    #[test]
    fn a_float_is_laid_out_from_its_digits_and_its_exponent() {
        let pipes = |parts: &[&str]| parts.join(" || ");
        let text =
            TextFunctions { substring: "SUBSTR", trim_fraction: &rtrim_fraction, concat: &pipes };
        let laid_out =
            float_text(&FloatParts { digits: "d", exponent: "x", exponent_digits: "w" }, 15, &text);
        assert_eq!(
            laid_out,
            "CASE WHEN x < -4 OR x >= 15 THEN RTRIM(RTRIM(SUBSTR(d, 1, 1) || '.' || \
             SUBSTR(d, 2, 15), '0'), '.') || 'e' || CASE WHEN x < 0 THEN '-' ELSE '+' END || \
             CASE WHEN ABS(x) < 10 THEN '0' ELSE '' END || w \
             WHEN x < 0 THEN RTRIM(RTRIM(SUBSTR('0.000', 1, 1 - x) || d, '0'), '.') \
             ELSE RTRIM(RTRIM(SUBSTR(d, 1, x + 1) || '.' || SUBSTR(d, x + 2, 15), '0'), '.') END"
        );
        assert_eq!(
            signed_float_text("v", "m", &pipes),
            "CASE WHEN v IS NULL THEN NULL WHEN v = 0 THEN '0' \
             ELSE CASE WHEN v < 0 THEN '-' ELSE '' END || m END"
        );
    }

    #[test]
    fn a_float_is_written_through_the_function_for_its_digits() {
        assert_eq!(float_text_function(DOUBLE_DIGITS), FLOAT_TEXT_FUNCTION);
        assert_eq!(float_text_function(SINGLE_DIGITS), SINGLE_FLOAT_TEXT_FUNCTION);
    }

    #[test]
    fn a_computed_float_breaks_before_each_when() {
        let arithmetic = FloatArithmetic {
            floor_log10: &|x| format!("FLOOR(LOG10({x}))"),
            power_of_ten: &|x| format!("POW(10, {x})"),
            whole_text: &|x| format!("CAST(CAST({x} AS DECIMAL(19, 0)) AS CHAR)"),
            text: TextFunctions {
                substring: "SUBSTRING",
                trim_fraction: &rtrim_fraction,
                concat: &|parts| format!("CONCAT({})", parts.join(", ")),
            },
        };
        let whole = computed_float_text("x", DOUBLE_DIGITS, &arithmetic);
        let lines = in_short_lines(&whole, "  ");
        assert_eq!(lines.replace("\n  WHEN ", " WHEN "), whole, "only the line breaks differ");
        let longest = lines.lines().map(str::len).max().unwrap_or_default();
        assert!(
            whole.len() > 3000 && longest < 400,
            "{} characters in one line become lines of at most {longest}",
            whole.len()
        );
    }

    #[test]
    fn the_fraction_is_trimmed_after_its_zeros() {
        assert_eq!(rtrim_fraction("x"), "RTRIM(RTRIM(x, '0'), '.')");
    }
}