Skip to main content

razeri_plugin_contract/
capabilities.rs

1// Copyright (C) 2026 Gabriel Shea
2// Licensed under either of the Apache License, Version 2.0 or the MIT
3// license, at your option.
4
5//! Boolean capability flags with conservative defaults.
6//!
7//! [`DialectCapabilities`] tells the rendering layer what syntax features
8//! the platform supports. Defaults are conservative (`false` / ANSI-safe),
9//! so unknown platforms produce safe SQL.
10
11use crate::data_type::DataType;
12use crate::engine_version::EngineVersion;
13use crate::evolution::EvolutionAnswers;
14use crate::merge_shape::MergeShape;
15
16/// **What one engine holds in a text column**: the longest `varchar(n)` it
17/// takes, how that length is counted, where the number came from, and what an
18/// unbounded `text` column cannot be there.
19///
20/// Answered by [`DialectCapabilities::text_limits`]. A length past the
21/// longest is refused by name before anything is rendered, as a decimal wider
22/// than the engine holds is.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[non_exhaustive]
25pub struct TextLimits {
26    /// The longest `varchar(n)` a column takes, where the engine has one.
27    pub longest_varchar: Option<u32>,
28    /// How [`Self::longest_varchar`] is counted.
29    pub counted_in: VarcharUnit,
30    /// Where the number came from.
31    pub source: LimitSource,
32    /// What the engine refuses an unbounded `text` column as, where it
33    /// refuses it as a key — `a key or UNIQUE`, `a primary key`.
34    pub text_is_not: Option<&'static str>,
35}
36
37impl TextLimits {
38    /// A longest `varchar`, counted in `counted_in`, from `source`.
39    #[must_use]
40    pub const fn new(longest_varchar: u32, counted_in: VarcharUnit, source: LimitSource) -> Self {
41        Self { longest_varchar: Some(longest_varchar), counted_in, source, text_is_not: None }
42    }
43
44    /// The same limits, for an engine that refuses an unbounded `text` as
45    /// `what` — `a key or UNIQUE`.
46    #[must_use]
47    pub const fn and_text_is_not(self, what: &'static str) -> Self {
48        Self { text_is_not: Some(what), ..self }
49    }
50
51    /// Why `varchar(length)` does not fit, in a few plain words, or `None`
52    /// when it does.
53    #[must_use]
54    pub fn misfit(&self, length: u32) -> Option<String> {
55        let longest = self.longest_varchar?;
56        (length > longest).then(|| {
57            format!(
58                "its varchar holds at most {longest} {}, and this asks for {length}",
59                self.counted_in.word()
60            )
61        })
62    }
63}
64
65/// **How an engine counts a `varchar`'s declared length.**
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67#[non_exhaustive]
68pub enum VarcharUnit {
69    /// In characters, whatever each takes to store.
70    Characters,
71    /// In bytes: a character outside ASCII takes more than one.
72    Bytes,
73}
74
75impl VarcharUnit {
76    /// The unit as a sentence says it: `characters`, `bytes`.
77    #[must_use]
78    pub const fn word(self) -> &'static str {
79        match self {
80            Self::Characters => "characters",
81            Self::Bytes => "bytes",
82        }
83    }
84}
85
86/// **Where a stated limit came from.**
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88#[non_exhaustive]
89pub enum LimitSource {
90    /// Measured on the engine: one length past it refused at `CREATE TABLE`.
91    MeasuredOnTheEngine,
92    /// Read from the engine's documentation; no engine was run.
93    TheEnginesDocumentation,
94}
95
96/// **One stated limit**: the most an engine holds, and where the number came
97/// from.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub struct Limit {
100    /// The most the engine holds: digits of a second, or a length.
101    pub most: u32,
102    /// Where the number came from.
103    pub source: LimitSource,
104}
105
106impl Limit {
107    /// The most measured on the engine: that many created, one more refused.
108    #[must_use]
109    pub const fn measured(most: u32) -> Self {
110        Self { most, source: LimitSource::MeasuredOnTheEngine }
111    }
112
113    /// The most the engine's documentation states; no engine was run.
114    #[must_use]
115    pub const fn documented(most: u32) -> Self {
116        Self { most, source: LimitSource::TheEnginesDocumentation }
117    }
118}
119
120/// **What one engine's columns hold, past its decimals and its varchars** —
121/// the answer to [`DialectCapabilities::type_limits`].
122///
123/// `None` in a field means *nothing stated*: the engine stores the type as
124/// text that holds any value, or the number has not been recorded. Either way
125/// nothing is refused for it. Start from [`TypeLimits::NONE`] and fill in
126/// what your engine has.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub struct TypeLimits {
129    /// The most digits of a second this dialect's spelling of a `timestamp`,
130    /// `timestamptz` or `datetime` keeps.
131    pub timestamp_digits: Option<Limit>,
132    /// The most digits of a second this dialect's spelling of a `time` keeps.
133    pub time_digits: Option<Limit>,
134    /// The longest `char(n)`, counted in [`Self::char_counted_in`].
135    pub longest_char: Option<Limit>,
136    /// How [`Self::longest_char`] is counted.
137    pub char_counted_in: VarcharUnit,
138    /// The longest `binary(n)`, in bytes.
139    pub longest_binary: Option<Limit>,
140    /// The longest `varbinary(n)`, in bytes.
141    pub longest_varbinary: Option<Limit>,
142}
143
144impl TypeLimits {
145    /// Nothing stated: every field `None`, a `char` counted in characters.
146    pub const NONE: Self = Self {
147        timestamp_digits: None,
148        time_digits: None,
149        longest_char: None,
150        char_counted_in: VarcharUnit::Characters,
151        longest_binary: None,
152        longest_varbinary: None,
153    };
154
155    /// **Why the engine cannot hold `data_type`**, or `None` when it can.
156    ///
157    /// Only the type itself is asked, not a type nested inside it: a caller
158    /// walks an array's or a record's members itself.
159    #[must_use]
160    pub fn misfit(&self, data_type: &DataType) -> Option<Misfit> {
161        match data_type {
162            DataType::Timestamp(moment) => {
163                self.too_many_digits(moment.precision, |most| format!("timestamp({most})"))
164            }
165            // A design spells a zoned timestamp and a datetime only at six
166            // digits, which every engine that states a limit keeps.
167            DataType::TimestampTz(moment) => {
168                self.too_many_digits(moment.precision, |_| "timestamptz".to_owned())
169            }
170            DataType::DateTime(moment) => {
171                self.too_many_digits(moment.precision, |_| "datetime".to_owned())
172            }
173            DataType::Time(time) => {
174                let limit = self.time_digits?;
175                // A bare `time` keeps the engine's own, which it holds.
176                let asked = u32::from(time.precision?);
177                (asked > limit.most).then(|| Misfit {
178                    why: if limit.most == 0 {
179                        format!(
180                            "its times of day keep whole seconds only, and this asks for {asked} \
181                             digits of a second"
182                        )
183                    } else {
184                        format!(
185                            "its times of day keep at most {} digits of a second, and this asks \
186                             for {asked}",
187                            limit.most
188                        )
189                    },
190                    kind: "time",
191                    fits: if limit.most == 0 {
192                        "time".to_owned()
193                    } else {
194                        format!("time({})", limit.most)
195                    },
196                })
197            }
198            DataType::Char(text) => {
199                let limit = self.longest_char?;
200                (text.length > limit.most).then(|| Misfit {
201                    why: format!(
202                        "its char holds at most {} {}, and this asks for {}",
203                        limit.most,
204                        self.char_counted_in.word(),
205                        text.length
206                    ),
207                    kind: "char",
208                    fits: format!("char({})", limit.most),
209                })
210            }
211            DataType::Binary(bytes) => {
212                let limit = self.longest_binary?;
213                (bytes.length > limit.most).then(|| Misfit {
214                    why: format!(
215                        "its binary holds at most {} bytes, and this asks for {}",
216                        limit.most, bytes.length
217                    ),
218                    kind: "binary",
219                    fits: format!("binary({})", limit.most),
220                })
221            }
222            DataType::VarBinary(bytes) => {
223                let limit = self.longest_varbinary?;
224                let asked = bytes.max_length?;
225                (asked > limit.most).then(|| Misfit {
226                    why: format!(
227                        "its varbinary holds at most {} bytes, and this asks for {asked}",
228                        limit.most
229                    ),
230                    kind: "varbinary",
231                    fits: format!("varbinary({})", limit.most),
232                })
233            }
234            _ => None,
235        }
236    }
237
238    /// A timestamp's digits of a second against [`Self::timestamp_digits`].
239    fn too_many_digits(&self, asked: u8, fits: impl Fn(u32) -> String) -> Option<Misfit> {
240        let limit = self.timestamp_digits?;
241        (u32::from(asked) > limit.most).then(|| Misfit {
242            why: format!(
243                "its timestamps keep at most {} digits of a second, and this asks for {asked}",
244                limit.most
245            ),
246            kind: "timestamp",
247            fits: fits(limit.most),
248        })
249    }
250}
251
252/// **Why a column type does not fit an engine**, in the words a refusal uses.
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct Misfit {
255    /// What the engine holds and what was asked, in a few plain words:
256    /// `its timestamps keep at most 6 digits of a second, and this asks for 12`.
257    pub why: String,
258    /// The kind of type, as a refusal names it: `timestamp`, `char`.
259    pub kind: &'static str,
260    /// The nearest type the engine holds, as a design declares it:
261    /// `timestamp(6)`, `char(255)`.
262    pub fits: String,
263}
264
265/// **The widest exact decimal one engine's column holds.**
266///
267/// Every engine that records one refuses a precision of zero, so the fewest
268/// digits is always one and is not a field.
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270pub struct DecimalLimits {
271    /// The most digits a column may declare.
272    pub max_precision: u16,
273    /// The most digits after the decimal point.
274    pub max_scale: u16,
275    /// Whether the digits after the point must be no more than the digits in
276    /// all. Some engines let a column hold only a fraction smaller than a
277    /// thousandth, with more scale than precision; most refuse it.
278    pub scale_within_precision: bool,
279    /// The most digits before the decimal point, where the engine bounds
280    /// them apart from the digits in all; `None` where the digits in all are
281    /// the only bound. `BigQuery` holds 76 digits, but never more than 38 of
282    /// them before the point.
283    pub max_integer_digits: Option<u16>,
284}
285
286impl DecimalLimits {
287    /// Why `decimal(precision, scale)` does not fit, in a few plain words, or
288    /// `None` when it does.
289    ///
290    /// Parameters:
291    /// - `precision`: the declared digits in all;
292    /// - `scale`: the declared digits after the point.
293    #[must_use]
294    pub fn misfit(&self, precision: u16, scale: u16) -> Option<String> {
295        let most = self.max_precision;
296        if precision == 0 || precision > most {
297            return Some(format!(
298                "its decimals hold 1 to {most} digits, and this asks for {precision}"
299            ));
300        }
301        if scale > self.max_scale {
302            return Some(format!(
303                "its decimals hold at most {} digits after the point, and this asks for {scale}",
304                self.max_scale
305            ));
306        }
307        if self.scale_within_precision && scale > precision {
308            return Some(format!(
309                "its decimals hold no more digits after the point than in all, and this asks for \
310                 {scale} of {precision}"
311            ));
312        }
313        let before_point = precision.saturating_sub(scale);
314        if let Some(most) = self.max_integer_digits
315            && before_point > most
316        {
317            return Some(format!(
318                "its decimals hold at most {most} digits before the point, and this asks for \
319                 {before_point}"
320            ));
321        }
322        None
323    }
324
325    /// The nearest `decimal(precision, scale)` these limits hold, for a
326    /// refusal to suggest in place of the one declared.
327    ///
328    /// Parameters:
329    /// - `precision`: the declared digits in all;
330    /// - `scale`: the declared digits after the point.
331    #[must_use]
332    pub fn nearest(&self, precision: u16, scale: u16) -> (u16, u16) {
333        let mut scale = scale.min(self.max_scale);
334        let mut precision = precision.clamp(1, self.max_precision);
335        if self.scale_within_precision {
336            scale = scale.min(precision);
337        }
338        if let Some(most) = self.max_integer_digits {
339            precision = precision.min(scale + most).max(1);
340        }
341        if self.scale_within_precision {
342            scale = scale.min(precision);
343        }
344        (precision, scale)
345    }
346}
347
348/// Level 6 — Boolean capability flags with conservative defaults.
349///
350/// These flags tell the rendering layer what syntax features the platform supports.
351/// Defaults are `false` / conservative, so unknown platforms produce safe ANSI SQL.
352pub trait DialectCapabilities {
353    // ═══════════════════════════════════════════════════════════════
354    // DML Capabilities
355    // ═══════════════════════════════════════════════════════════════
356
357    /// **How this engine performs the upsert Razeri's generated load is.**
358    ///
359    /// See [`MergeShape`]. The default is a refusal, because the honest answer
360    /// for an engine nobody has measured is that Razeri does not know how to
361    /// load it — not that it spells its upsert the way `MySQL` does.
362    fn merge_shape(&self) -> MergeShape {
363        MergeShape::None
364    }
365
366    /// **How much of the deployed schema a reconciliation can compare with
367    /// the design here.**
368    ///
369    /// Razeri's schema check reads the engine's catalogue in one query: the
370    /// ANSI `information_schema.columns`, a `FULL OUTER JOIN` against the
371    /// design's own columns, and a table value constructor with a derived
372    /// column list, `(VALUES ('a'), ('b')) AS designed (column_name)`. See
373    /// [`CatalogueReach`]. The default is
374    /// [`CatalogueReach::Nothing`] with a sentence saying Razeri has not
375    /// written this engine's catalogue query: an engine nobody has measured
376    /// gets no check, and a warning that says why, rather than a query that
377    /// fails.
378    fn catalogue_reach(&self) -> CatalogueReach {
379        CatalogueReach::Nothing(
380            "razeri has not written this platform's catalog query, and guessing at one would \
381             emit SQL that fails rather than a verdict",
382        )
383    }
384
385    /// **What this engine keeps when a table that already holds rows changes
386    /// shape.**
387    ///
388    /// See [`EvolutionAnswers`]. The default answers nothing, which Razeri
389    /// reads as *nobody has measured this engine* — so a change Razeri cannot
390    /// otherwise prove keeps every value is reported as one that does not, and
391    /// an operator gets a warning they can read instead of a surprise.
392    fn evolution_answers(&self) -> EvolutionAnswers {
393        EvolutionAnswers::none()
394    }
395
396    /// Whether the platform supports INSERT ... ON CONFLICT (PostgreSQL-style upsert).
397    fn supports_on_conflict(&self) -> bool {
398        false
399    }
400
401    /// Whether the platform supports INSERT OVERWRITE.
402    fn supports_insert_overwrite(&self) -> bool {
403        false
404    }
405
406    /// Whether the platform supports UPDATE ... FROM.
407    fn supports_update_from(&self) -> bool {
408        false
409    }
410
411    /// Whether the platform supports RETURNING / OUTPUT clauses.
412    fn supports_returning(&self) -> bool {
413        false
414    }
415
416    // ═══════════════════════════════════════════════════════════════
417    // The server's version
418    // ═══════════════════════════════════════════════════════════════
419
420    /// **How to ask a running server its version**, or `None` where this
421    /// engine has no version a user chooses, or nobody has written the
422    /// question.
423    ///
424    /// The query answers one row whose first word is the version in the
425    /// spelling this dialect declares (`17`, `8.4`, `23ai`); anything after it
426    /// is the server's own words, which a refusal prints. A generated deploy
427    /// script sends it after login and before the schema, and refuses a server
428    /// older than the version the bundle was rendered for, naming both. The
429    /// default is `None`, and a script that has no question asks none.
430    fn server_version_query(&self) -> Option<&str> {
431        None
432    }
433
434    /// **The version in one answer to [`Self::server_version_query`]**, or
435    /// `None` when the answer holds none.
436    ///
437    /// Parameters:
438    /// - `answer`: the one row the query answered, as text.
439    ///
440    /// The default reads the first word, which is what a deploy script reads
441    /// too; override it only to read more of the answer than that.
442    fn parse_server_version(&self, answer: &str) -> Option<EngineVersion> {
443        answer.split_whitespace().next().and_then(|word| EngineVersion::parse(word).ok())
444    }
445
446    // ═══════════════════════════════════════════════════════════════
447    // Query Capabilities
448    // ═══════════════════════════════════════════════════════════════
449
450    /// Whether the platform supports Common Table Expressions (WITH).
451    fn supports_cte(&self) -> bool {
452        true
453    }
454
455    /// Whether the platform supports recursive CTEs.
456    fn supports_recursive_cte(&self) -> bool {
457        true
458    }
459
460    /// Whether the platform supports window functions.
461    fn supports_window_functions(&self) -> bool {
462        true
463    }
464
465    /// Whether the platform supports named windows (WINDOW clause).
466    fn supports_named_windows(&self) -> bool {
467        true
468    }
469
470    /// Whether the platform supports QUALIFY clause.
471    fn supports_qualify(&self) -> bool {
472        false
473    }
474
475    /// Whether the platform supports GROUPS frame type in window functions.
476    fn supports_groups_frame(&self) -> bool {
477        false
478    }
479
480    /// Whether the platform supports NULLS FIRST/LAST in ORDER BY.
481    fn supports_nulls_ordering(&self) -> bool {
482        true
483    }
484
485    // ═══════════════════════════════════════════════════════════════
486    // DDL Capabilities
487    // ═══════════════════════════════════════════════════════════════
488
489    /// Whether the platform supports IDENTITY columns.
490    fn supports_identity(&self) -> bool {
491        false
492    }
493
494    /// Whether the platform supports sequences (CREATE SEQUENCE).
495    fn supports_sequences(&self) -> bool {
496        false
497    }
498
499    /// Whether the platform supports schema namespacing.
500    fn supports_schemas(&self) -> bool {
501        true
502    }
503
504    /// **Can a routine created in one schema be called from a routine created
505    /// in another, with nothing else done?**
506    ///
507    /// True nearly everywhere, and true by default: a schema on most engines is
508    /// a namespace, and naming an object in another namespace is all that
509    /// reaching it takes.
510    ///
511    /// Answer `false` where a schema is also an owner and reaching across one
512    /// costs a privilege granted per object. On Oracle a schema IS a user; a
513    /// routine runs with its owner's privileges, so a function created in
514    /// `acme_core` calling one created in `acme_raw` compiles with errors —
515    /// `ORA-60535: invalid identifier`, measured — however exactly it spells
516    /// the name.
517    ///
518    /// What Razeri does with the answer is put a copy of the key function in
519    /// **every namespace that calls it** instead of one per domain, so each
520    /// namespace stands on its own and no deploy owes a grant. It is one
521    /// derivation either way: the copies are the same builder over the same
522    /// vocabulary, not two spellings free to drift. The engine-level hash
523    /// beneath them keeps its single home — see
524    /// [`ReplicationRenderer::sid_prerequisite_home`](crate::ReplicationRenderer::sid_prerequisite_home)
525    /// — because that one is Razeri's own object and installs its own grant.
526    fn routine_is_callable_across_schemas(&self) -> bool {
527        true
528    }
529
530    /// Whether the platform supports bulk load (COPY, BULK INSERT, etc.).
531    fn supports_bulk_load(&self) -> bool {
532        false
533    }
534
535    /// **Whether a generated load is written as a stored procedure** on this
536    /// engine.
537    ///
538    /// Razeri asks this before it writes a load as a procedure, and asks
539    /// [`SqlRenderer::procedure_form`](crate::SqlRenderer::procedure_form)
540    /// how to spell one. The default is `false`, and a load is then written as
541    /// plain statements. A dialect that answers `procedure_form` with
542    /// `ProcedureForm::Custom` answers `true` here, or its procedures are
543    /// never asked for; one that answers `ProcedureForm::Unsupported` leaves
544    /// this `false`. The conformance report holds the two to each other.
545    fn supports_stored_procedures(&self) -> bool {
546        false
547    }
548
549    /// Whether the platform **stores functions at all** — a routine the schema
550    /// creates and later calls by name, in any spelling.
551    ///
552    /// Nearly every engine razeri reaches has one: `CREATE FUNCTION` on
553    /// `PostgreSQL`, `MySQL`, `MariaDB`, Snowflake, `SQL Server` and Databricks,
554    /// a `MACRO` on `DuckDB`, a lambda `CREATE FUNCTION` on `ClickHouse`.
555    ///
556    /// **`SQLite` answers `false`, and it is the reason this question is asked
557    /// out loud.** `SQLite` has no `CREATE FUNCTION` statement in any spelling
558    /// and no `CREATE PROCEDURE` either — a function there is registered by the
559    /// program that opens the database, so nothing written into a schema script
560    /// can create one. Inheriting the `PostgreSQL` form wrote
561    /// `CREATE OR REPLACE FUNCTION … LANGUAGE SQL RETURN …` into `schema.sql`,
562    /// which `SQLite` answers with `near "CREATE": syntax error` part way
563    /// through the script, so the build refuses in front of the script instead.
564    ///
565    /// The default is `true`, matching [`Self::supports_table_functions`] and
566    /// for the same reason: refusing is the destructive answer, so a refusal
567    /// fires only where an engine is KNOWN to have no form, never because a
568    /// dialect forgot to answer. That reverses the conservative-`false` default
569    /// this trait uses for flags that *add* syntax — here `false` is the
570    /// unusual claim, and it is made deliberately, one dialect at a time.
571    fn supports_stored_functions(&self) -> bool {
572        true
573    }
574
575    /// Whether the platform has a **table-valued function** — a routine a query
576    /// selects FROM, returning columns rather than one value.
577    ///
578    /// Most engines have one in some spelling: `RETURNS TABLE(…)` on
579    /// `PostgreSQL`, Snowflake and Databricks, an inline TVF on SQL Server, a
580    /// table `MACRO` on `DuckDB`, a parameterized view on `ClickHouse`.
581    ///
582    /// **`MySQL` and `MariaDB` answer `false`, and they are the reason this
583    /// question exists.** A stored function there returns exactly one value;
584    /// there is no clause that returns a relation, in any spelling. Rendering
585    /// `RETURNS TABLE` for them produced `ERROR 1064 … syntax error` in the
586    /// middle of a schema script (measured on `MySQL` 8.4 and `MariaDB` 11.4),
587    /// so the build refuses in front of the script instead.
588    ///
589    /// The default is `true`: a refusal fires only where an engine is KNOWN to
590    /// have no form, never because a dialect forgot to answer.
591    fn supports_table_functions(&self) -> bool {
592        true
593    }
594
595    /// Whether the platform supports tasks / scheduled jobs.
596    fn supports_tasks(&self) -> bool {
597        false
598    }
599
600    /// Whether the platform supports native table partitioning.
601    fn supports_partitioning(&self) -> bool {
602        true
603    }
604
605    /// Whether the platform supports materialized views.
606    fn supports_materialized_views(&self) -> bool {
607        true
608    }
609
610    /// Whether the platform supports generated/computed columns.
611    fn supports_generated_columns(&self) -> bool {
612        true
613    }
614
615    /// Whether the platform supports JSONB or native JSON type.
616    fn supports_json(&self) -> bool {
617        true
618    }
619
620    // ═══════════════════════════════════════════════════════════════
621    // Type Capabilities
622    // ═══════════════════════════════════════════════════════════════
623
624    /// Whether the platform supports unsigned integers natively.
625    fn supports_unsigned_integers(&self) -> bool {
626        false
627    }
628
629    /// Whether the platform supports arrays natively.
630    fn supports_arrays(&self) -> bool {
631        false
632    }
633
634    /// **Whether this engine has a type of its own for a nested type, or
635    /// stores it as something else.**
636    ///
637    /// A nested column is an array, a map or a record. Some engines have all
638    /// three; some have one; some have none and hold the value as JSON or as
639    /// text. Razeri never refuses the column — the design is portable and it is
640    /// the storage that degrades — but it does say so once, naming the engine,
641    /// the columns and what each became.
642    ///
643    /// The default is `false`, which is the honest answer for an engine nobody
644    /// has answered for: the reader is told what the column became rather than
645    /// left to assume the type survived.
646    ///
647    /// Parameters:
648    /// - `data_type`: the nested type in question.
649    fn renders_nested_type_natively(&self, data_type: &DataType) -> bool {
650        let _ = data_type;
651        false
652    }
653
654    /// **Whether this engine can tell two values of a nested type apart.**
655    ///
656    /// Change detection asks whether an arriving value differs from the
657    /// standing one. For a scalar every engine answers with `<>`. For a nested
658    /// value they do not agree: some compare arrays, maps and records directly,
659    /// some refuse the comparison outright, and some hold the value as text and
660    /// compare the text. A column this returns `false` for is left out of
661    /// change detection and named in a note, rather than compared with an
662    /// operator the engine will reject.
663    ///
664    /// The default is `false` — an unanswered engine leaves the column
665    /// unwatched and says so, which is the failure that can be read.
666    ///
667    /// Parameters:
668    /// - `data_type`: the nested type in question.
669    fn compares_nested_values(&self, data_type: &DataType) -> bool {
670        let _ = data_type;
671        false
672    }
673
674    /// **Whether this engine can collapse rows on a nested column** — group by
675    /// it, or select distinct over it.
676    ///
677    /// A load reads its staged rows through one collapsed form so that a row
678    /// stated twice, identically, is one arriving row rather than a conflict.
679    /// The plain spelling of that collapse is `SELECT DISTINCT` over every
680    /// column, and grouping is what `DISTINCT` is: an engine that will not
681    /// group a value of a nested type will not take the column in a `DISTINCT`
682    /// either.
683    ///
684    /// It is a separate question from [`Self::compares_nested_values`], and the
685    /// two engines that answer them differently are the reason it is asked
686    /// separately: `BigQuery` compares two records with `=` and refuses to group
687    /// on one.
688    ///
689    /// Where the answer is `false` the collapse is written as a `GROUP BY` over
690    /// the other columns with [`crate::SqlRenderer::render_any_value`] around
691    /// this one.
692    ///
693    /// The default is `true`: `SELECT DISTINCT` over every column is the plain
694    /// spelling, and an engine says otherwise only where somebody has found
695    /// that it refuses.
696    ///
697    /// Asked only of a nested type — an array, a map or a record. The answer
698    /// for anything else is not read.
699    ///
700    /// Parameters:
701    /// - `data_type`: the nested type in question.
702    fn groups_nested_values(&self, data_type: &DataType) -> bool {
703        let _ = data_type;
704        true
705    }
706
707    /// Whether the platform supports native BOOLEAN type (vs BIT).
708    fn supports_boolean(&self) -> bool {
709        true
710    }
711
712    // ═══════════════════════════════════════════════════════════════
713    // Limits & Defaults
714    // ═══════════════════════════════════════════════════════════════
715
716    /// The default schema name for this platform.
717    fn default_schema(&self) -> &'static str {
718        "public"
719    }
720
721    /// Maximum identifier length for this platform.
722    fn max_identifier_length(&self) -> usize {
723        63
724    }
725
726    /// Maximum number of columns in a table (approximate).
727    fn max_columns(&self) -> usize {
728        10000
729    }
730
731    /// Maximum query parameters (approximate, 0 = unlimited).
732    fn max_parameters(&self) -> usize {
733        0
734    }
735
736    /// **The widest exact decimal a column on this platform holds**, where it
737    /// has been measured against the engine.
738    ///
739    /// A type over it is refused by name rather than rendered: the engine
740    /// would refuse the `CREATE TABLE` anyway, later and in its own words.
741    ///
742    /// The default is `None`, which means *not measured here*, not *no
743    /// limit*. An engine with no limit at all says so in its own doc comment
744    /// and still answers `None`.
745    fn decimal_limits(&self) -> Option<DecimalLimits> {
746        None
747    }
748
749    /// **What a text column on this platform holds**, where it has been
750    /// measured against the engine or read from its documentation (see
751    /// [`TextLimits`]).
752    ///
753    /// The default is `None`, which means *not recorded here*, not *no
754    /// limit*. Measure the longest `varchar(n)` your engine creates — one
755    /// more is refused at `CREATE TABLE` — and say how it counts it.
756    fn text_limits(&self) -> Option<TextLimits> {
757        None
758    }
759
760    /// **What this engine's other columns hold** — the digits of a second a
761    /// timestamp and a time of day keep, and the longest `char(n)`,
762    /// `binary(n)` and `varbinary(n)` (see [`TypeLimits`]).
763    ///
764    /// A type over a stated limit is refused by name before anything is
765    /// rendered. The default is `None`: nothing is stated, and nothing is
766    /// refused for this engine, which then refuses the `CREATE TABLE` later in
767    /// its own words. Measure each the way the varchar is measured — the most
768    /// your engine creates, one more refused — and leave `None` in a field
769    /// your engine stores with no limit.
770    fn type_limits(&self) -> Option<TypeLimits> {
771        None
772    }
773
774    /// **Whether this engine has a type that keeps a time zone**, for a
775    /// `timestamptz` column: a moment that keeps its offset, or one read in
776    /// the session's zone. The default is `true`. An engine with no such type
777    /// answers `false`, and a `timestamptz` column is refused by name rather
778    /// than stored as a clock time two hours off the moment the file named.
779    fn keeps_a_time_zone(&self) -> bool {
780        true
781    }
782
783    // ═══════════════════════════════════════════════════════════════
784    // Drop Capabilities
785    // ═══════════════════════════════════════════════════════════════
786
787    /// Whether DROP supports CASCADE modifier.
788    fn supports_cascade_drop(&self) -> bool {
789        true
790    }
791
792    /// Whether DROP FUNCTION requires parameter types for overloaded functions.
793    fn supports_function_overloading(&self) -> bool {
794        false
795    }
796
797    /// Whether DROP supports SYNC modifier (`ClickHouse`).
798    fn supports_sync_drop(&self) -> bool {
799        false
800    }
801
802    /// Whether DROP supports FORCE modifier (Databricks).
803    fn supports_force_drop(&self) -> bool {
804        false
805    }
806
807    // ═══════════════════════════════════════════════════════════════
808    // DML Extended Capabilities
809    // ═══════════════════════════════════════════════════════════════
810
811    /// Whether the platform supports compound assignment (e.g., `col += value`).
812    fn supports_compound_assignment(&self) -> bool {
813        false
814    }
815
816    /// Whether the platform supports EXCLUDE clause in window frame specs.
817    fn supports_window_frame_exclude(&self) -> bool {
818        false
819    }
820
821    /// Whether the platform supports MERGE NOT MATCHED BY SOURCE.
822    fn supports_merge_not_matched_by_source(&self) -> bool {
823        false
824    }
825
826    /// Keyword emitted for MERGE NOT MATCHED branch.
827    fn merge_not_matched_keyword(&self) -> &'static str {
828        "WHEN NOT MATCHED"
829    }
830
831    /// Whether the platform supports UPDATE SET * / INSERT * in MERGE.
832    fn supports_merge_star_syntax(&self) -> bool {
833        false
834    }
835
836    /// Whether an UPDATE is spelled `ALTER TABLE … UPDATE` (`ClickHouse`).
837    ///
838    /// **There is a second declaration of the same fact** on
839    /// [`crate::SqlRenderer::uses_alter_table_update`], and that one is the
840    /// declaration Razeri's generators consult. A dialect that overrides one
841    /// must override both and keep them agreeing; the duplication is named
842    /// here so a reader finds it before an ambiguous-method error does.
843    fn uses_alter_table_update(&self) -> bool {
844        false
845    }
846
847    /// Whether the platform supports multi-table INSERT (Snowflake).
848    fn supports_multi_table_insert(&self) -> bool {
849        false
850    }
851
852    // ═══════════════════════════════════════════════════════════════
853    // DDL / Namespace Capabilities
854    // ═══════════════════════════════════════════════════════════════
855
856    /// The SQL keyword for the schema-level namespace.
857    ///
858    /// Most platforms use `SCHEMA` (`CREATE SCHEMA IF NOT EXISTS ...`).
859    /// `ClickHouse` uses `DATABASE` (`CREATE DATABASE IF NOT EXISTS ...`).
860    fn schema_namespace_keyword(&self) -> &'static str {
861        "SCHEMA"
862    }
863
864    // ═══════════════════════════════════════════════════════════════
865    // Comment Capabilities
866    // ═══════════════════════════════════════════════════════════════
867
868    /// Whether the platform supports `#` as a single-line comment delimiter.
869    ///
870    /// Default: `false`. A dialect whose engine accepts `#` as a single-line
871    /// comment delimiter overrides this.
872    fn supports_hash_comments(&self) -> bool {
873        false
874    }
875
876    // ═══════════════════════════════════════════════════════════════
877    // Script Formatting
878    // ═══════════════════════════════════════════════════════════════
879
880    /// Batch separator keyword required between DDL statements in a script.
881    ///
882    /// SQL Server / T-SQL requires `GO` between `CREATE FUNCTION`, `CREATE VIEW`,
883    /// and `CREATE PROCEDURE` statements. Other platforms return `None` and use
884    /// double-newlines between statements.
885    fn batch_separator(&self) -> Option<&'static str> {
886        None
887    }
888
889    // ═══════════════════════════════════════════════════════════════
890    // Transaction Capabilities
891    // ═══════════════════════════════════════════════════════════════
892
893    /// Whether several statements in a script can be committed or discarded
894    /// together as one unit.
895    ///
896    /// A generated load is a sequence of writes that only make sense finished.
897    /// An SCD-2 load, for instance, closes the standing version into `_history`
898    /// and then re-opens `_current` on the new attributes; a process that dies
899    /// between the two leaves a closed version that `_current` still holds open,
900    /// and because `_history` is keyed on (surrogate, version start), the next
901    /// load writes the same key again and the engine refuses it. Every load
902    /// after the crash fails until somebody deletes that row by hand.
903    ///
904    /// Wrapping the load in one transaction is what makes the crash a no-op
905    /// instead. This flag says whether the platform offers that at all. It
906    /// defaults to `true` because a transaction spanning several statements is
907    /// core SQL and nearly every engine has it; a platform that does not
908    /// overrides it, and the load script then says plainly that the operator
909    /// owns the boundary.
910    fn supports_multi_statement_transactions(&self) -> bool {
911        true
912    }
913
914    /// How this platform opens a transaction.
915    ///
916    /// The ANSI spelling, which `MySQL` and `MariaDB` also use. `DuckDB`,
917    /// `SQLite`, `SQL Server` and `Snowflake` say `BEGIN TRANSACTION`;
918    /// `PostgreSQL` and `Citus` say `BEGIN`.
919    fn begin_transaction_keyword(&self) -> &'static str {
920        "START TRANSACTION"
921    }
922
923    /// How this platform commits one.
924    ///
925    /// `COMMIT` everywhere razeri reaches except `SQL Server`, which spells it
926    /// `COMMIT TRANSACTION`.
927    fn commit_transaction_keyword(&self) -> &'static str {
928        "COMMIT"
929    }
930
931    /// **Whether this engine's begin statement leaves the commit to the
932    /// client.**
933    ///
934    /// On most engines [`begin_transaction_keyword`](Self::begin_transaction_keyword)
935    /// opens a transaction that holds every statement after it until the
936    /// commit, whatever the client does. On some it only names the
937    /// transaction — Oracle's and SAP HANA's `SET TRANSACTION READ WRITE` —
938    /// and a driver that commits each statement on its own still does, so a
939    /// load run that way keeps what it wrote when it stops part way.
940    ///
941    /// Razeri's own deploy scripts drive clients whose commit is off. Answer
942    /// `true` where the engine needs that of the client, and the bundle tells
943    /// whoever runs a load another way — a scheduler running the task list —
944    /// to turn its client's commit off. The default is `false`.
945    fn begin_leaves_the_commit_to_the_client(&self) -> bool {
946        false
947    }
948
949    /// Whether emptying a table with `TRUNCATE` ends the open transaction.
950    ///
951    /// On some engines `TRUNCATE TABLE` commits everything before it and
952    /// cannot be rolled back: `MySQL` and `MariaDB`, which treat it as a
953    /// schema change, and Oracle, SAP HANA, `BigQuery` and Redshift. A
954    /// generated load empties its staging table only as its last statement,
955    /// after every write it makes, so on those engines the clear commits the
956    /// load with it and each load stands on its own; a load that stops before
957    /// it leaves nothing behind. Everywhere else `TRUNCATE` is an ordinary
958    /// statement inside the transaction.
959    fn truncate_commits_implicitly(&self) -> bool {
960        false
961    }
962
963    /// **Whether a load's refusal stops the statement that asks it.**
964    ///
965    /// A generated load stops itself by building a sentence about the rows it
966    /// refuses and asking the engine for
967    /// [`ExpressionRenderer::render_refusal`](crate::ExpressionRenderer::render_refusal)
968    /// of it: the engine refuses and quotes the sentence back in its error.
969    /// Most engines do. `MySQL` and `MariaDB` answer the default refusal, a
970    /// conversion to a number, with a warning and `0`, and Spark answers
971    /// `NULL` where ANSI mode is off. There the load stops on a second
972    /// statement instead, in the engine's own words and without the sentence,
973    /// so a deploy script reads the refused rows back and prints them itself.
974    ///
975    /// The default is `true`, the answer of every engine whose refusal stops.
976    fn refusal_stops_the_statement(&self) -> bool {
977        true
978    }
979
980    /// **Whether this engine's file reader may read a field holding just the
981    /// word `NULL` as a missing value** rather than as the four letters.
982    ///
983    /// A design names the words that mean "no value" in each file, and a
984    /// binding that names none calls `NULL` missing, which every engine then
985    /// agrees on. A file that keeps `NULL` as text cannot be read that way
986    /// here, so the bundle names the file, and a reconciliation does not
987    /// compare the characters of a text column holding the word. The default
988    /// is `false`: the engine keeps the text, as every engine's own file
989    /// reader does except `MySQL`'s and `MariaDB`'s `LOAD DATA`.
990    fn delimited_file_reads_null_word_as_missing(&self) -> bool {
991        false
992    }
993
994    // ═══════════════════════════════════════════════════════════════
995    // Storage / Partition / Compression Capabilities
996    // ═══════════════════════════════════════════════════════════════
997
998    /// Whether the platform supports a server-side partition function definable
999    /// at design time (`T-SQL` `PARTITION FUNCTION`, `PostgreSQL` `PARTITION BY`,
1000    /// `MySQL` `PARTITION BY`, `ClickHouse` `PARTITION BY`).
1001    ///
1002    /// `Snowflake` / `DuckDB` return `false` because micro-partitioning is automatic
1003    /// and not user-definable.
1004    fn supports_partition_function(&self) -> bool {
1005        false
1006    }
1007
1008    /// Whether the platform supports adding/removing partition boundaries on a
1009    /// live table (`T-SQL` `ALTER PARTITION FUNCTION SPLIT/MERGE RANGE`,
1010    /// `PostgreSQL` `ATTACH/DETACH PARTITION`, `MySQL` `REORGANIZE PARTITION`).
1011    fn supports_partition_split_merge(&self) -> bool {
1012        false
1013    }
1014
1015    /// Whether the platform supports a columnstore index (`T-SQL` clustered/non-clustered
1016    /// columnstore; `ClickHouse` via `MergeTree` engine; not natively in `PostgreSQL` / `MySQL`).
1017    fn supports_columnstore_index(&self) -> bool {
1018        false
1019    }
1020
1021    /// Whether the platform realizes a columnstore as a **table storage engine**
1022    /// rather than an index (`MariaDB` `ENGINE=ColumnStore`). This is deliberately
1023    /// distinct from [`Self::supports_columnstore_index`]: a design that asks for
1024    /// `index: { kind: clustered_columnstore }` is honoured on such a platform by
1025    /// selecting the engine, but it must NOT inherit the `T-SQL`
1026    /// index machinery — the composite-PK demotion to `UNIQUE NONCLUSTERED` keys
1027    /// off `supports_columnstore_index`, and an engine-columnstore keeps its
1028    /// ordinary primary key. So the two capabilities are separate axes: "supports
1029    /// columnstore at all" is the disjunction of them.
1030    fn columnstore_is_table_engine(&self) -> bool {
1031        false
1032    }
1033
1034    /// Whether the platform has a clustering-key concept distinct from primary key
1035    /// (`Snowflake` `CLUSTER BY`, Delta Liquid Clustering / `Z-ORDER`, `ClickHouse` `ORDER BY`).
1036    fn supports_clustering_key(&self) -> bool {
1037        false
1038    }
1039
1040    /// Whether the platform supports user-controllable table compression
1041    /// (`T-SQL` `DATA_COMPRESSION = PAGE/ROW/COLUMNSTORE`, `MySQL` `ROW_FORMAT=COMPRESSED`,
1042    /// `ClickHouse` `CODEC(ZSTD)`, Delta `delta.compressionCodec`).
1043    fn supports_table_compression(&self) -> bool {
1044        false
1045    }
1046
1047    /// Whether the platform requires every column referenced by the partition
1048    /// key to also appear in the table's PRIMARY KEY (or any UNIQUE
1049    /// constraint).
1050    ///
1051    /// `PostgreSQL` enforces this with `SQLSTATE 0A000` ("unique constraint on
1052    /// partitioned table must include all partitioning columns"). `T-SQL`,
1053    /// `MySQL`, `ClickHouse`, `Snowflake`, and `DuckDB` do not impose this
1054    /// restriction.
1055    fn partition_requires_pk_includes_field(&self) -> bool {
1056        false
1057    }
1058}
1059
1060/// **How much of a deployed schema Razeri's reconciliation can read back from
1061/// this engine's catalogue**, answered by
1062/// [`DialectCapabilities::catalogue_reach`].
1063#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1064#[non_exhaustive]
1065pub enum CatalogueReach {
1066    /// **Every check.** The engine runs the query that compares its catalogue
1067    /// with the design in one measurement: `information_schema.columns`, a
1068    /// `FULL OUTER JOIN`, and a table value constructor with a derived column
1069    /// list.
1070    Whole,
1071    /// **The columns it holds, and not the comparison.** The engine has
1072    /// `information_schema.columns` and lacks what the one-measurement
1073    /// comparison needs, so the reconciliation asks only whether each designed
1074    /// column is held. The sentence says what it lacks; a warning carries it.
1075    HeldColumnsOnly(&'static str),
1076    /// **No schema check.** The sentence says why, in a reader's words; a
1077    /// warning carries it.
1078    Nothing(&'static str),
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083    use super::*;
1084    use crate::merge_shape::MergeRestrictions;
1085
1086    struct DefaultDialect;
1087    impl DialectCapabilities for DefaultDialect {}
1088
1089    /// **A dialect that states no question asks none, and the first word of
1090    /// an answer is its version.**
1091    #[test]
1092    fn the_server_version_is_the_first_word_of_the_answer() {
1093        let d = DefaultDialect;
1094        assert_eq!(d.server_version_query(), None);
1095        let read = d.parse_server_version("17 17.2");
1096        assert_eq!(read.map(|v| v.to_string()), Some("17".to_owned()));
1097        assert_eq!(d.parse_server_version("  8.4 MySQL Community Server"), "8.4".parse().ok());
1098        assert_eq!(d.parse_server_version(""), None);
1099        assert_eq!(d.parse_server_version("PostgreSQL 17.2"), None);
1100    }
1101
1102    /// Assert default DML capability flags are conservative.
1103    /// Parameters:
1104    /// - d: Dialect capabilities implementation under test.
1105    fn assert_default_dml_capabilities(d: &impl DialectCapabilities) {
1106        assert_eq!(d.merge_shape(), MergeShape::None);
1107        assert!(!d.supports_on_conflict());
1108        assert!(!d.supports_insert_overwrite());
1109        assert!(!d.supports_update_from());
1110        assert!(!d.supports_returning());
1111    }
1112
1113    /// Assert default query capability flags and ANSI-safe behavior.
1114    /// Parameters:
1115    /// - d: Dialect capabilities implementation under test.
1116    fn assert_default_query_capabilities(d: &impl DialectCapabilities) {
1117        assert!(d.supports_cte());
1118        assert!(d.supports_recursive_cte());
1119        assert!(d.supports_window_functions());
1120        assert!(d.supports_named_windows());
1121        assert!(!d.supports_qualify());
1122        assert!(!d.supports_groups_frame());
1123        assert!(d.supports_nulls_ordering());
1124    }
1125
1126    /// Assert default DDL capability and type flags are conservative.
1127    /// Parameters:
1128    /// - d: Dialect capabilities implementation under test.
1129    fn assert_default_ddl_and_type_capabilities(d: &impl DialectCapabilities) {
1130        assert!(!d.supports_identity());
1131        assert!(!d.supports_sequences());
1132        assert!(d.supports_schemas());
1133        assert!(!d.supports_bulk_load());
1134        assert!(!d.supports_stored_procedures());
1135        // The two routine-form questions default the other way on purpose:
1136        // answering `false` REFUSES a build, so it is never the answer an
1137        // unknown platform falls into.
1138        assert!(d.supports_stored_functions());
1139        assert!(d.supports_table_functions());
1140        assert!(!d.supports_tasks());
1141        assert!(d.supports_partitioning());
1142        assert!(d.supports_materialized_views());
1143        assert!(d.supports_generated_columns());
1144        assert!(d.supports_json());
1145        assert!(!d.supports_unsigned_integers());
1146        assert!(!d.supports_arrays());
1147        assert!(d.supports_boolean());
1148    }
1149
1150    /// Assert default platform limits and storage capability flags.
1151    /// Parameters:
1152    /// - d: Dialect capabilities implementation under test.
1153    fn assert_default_limits_and_storage_capabilities(d: &impl DialectCapabilities) {
1154        assert_eq!(d.default_schema(), "public");
1155        assert_eq!(d.max_identifier_length(), 63);
1156        assert_eq!(d.max_columns(), 10000);
1157        assert_eq!(d.max_parameters(), 0);
1158        assert!(!d.supports_partition_function());
1159        assert!(!d.supports_partition_split_merge());
1160        assert!(!d.supports_columnstore_index());
1161        assert!(!d.supports_clustering_key());
1162        assert!(!d.supports_table_compression());
1163    }
1164
1165    #[test]
1166    fn defaults_are_conservative() {
1167        let d = DefaultDialect;
1168        assert_default_dml_capabilities(&d);
1169        assert_default_query_capabilities(&d);
1170        assert_default_ddl_and_type_capabilities(&d);
1171        assert_default_limits_and_storage_capabilities(&d);
1172    }
1173
1174    #[test]
1175    fn overrides_work() {
1176        struct PostgresLike;
1177        impl DialectCapabilities for PostgresLike {
1178            fn merge_shape(&self) -> MergeShape {
1179                MergeShape::Merge(MergeRestrictions::none())
1180            }
1181            fn supports_on_conflict(&self) -> bool {
1182                true
1183            }
1184            fn supports_returning(&self) -> bool {
1185                true
1186            }
1187            fn supports_identity(&self) -> bool {
1188                true
1189            }
1190            fn supports_sequences(&self) -> bool {
1191                true
1192            }
1193            fn supports_arrays(&self) -> bool {
1194                true
1195            }
1196            fn max_columns(&self) -> usize {
1197                1600
1198            }
1199        }
1200
1201        let d = PostgresLike;
1202        assert!(d.merge_shape().merges());
1203        assert!(d.supports_on_conflict());
1204        assert!(d.supports_returning());
1205        assert!(d.supports_identity());
1206        assert!(d.supports_sequences());
1207        assert!(d.supports_arrays());
1208        assert_eq!(d.max_columns(), 1600);
1209        // Untouched defaults
1210        assert!(!d.supports_insert_overwrite());
1211        assert!(d.supports_cte());
1212    }
1213
1214    #[test]
1215    fn new_capability_defaults_are_conservative() {
1216        let d = DefaultDialect;
1217        // Drop capabilities
1218        assert!(d.supports_cascade_drop()); // true = ANSI default
1219        assert!(!d.supports_function_overloading());
1220        assert!(!d.supports_sync_drop());
1221        assert!(!d.supports_force_drop());
1222        // DML extended
1223        assert!(!d.supports_compound_assignment());
1224        assert!(!d.supports_window_frame_exclude());
1225        assert!(!d.supports_merge_not_matched_by_source());
1226        assert_eq!(d.merge_not_matched_keyword(), "WHEN NOT MATCHED");
1227        assert!(!d.supports_merge_star_syntax());
1228        assert!(!d.uses_alter_table_update());
1229        assert!(!d.supports_multi_table_insert());
1230        // DDL / Namespace
1231        assert_eq!(d.schema_namespace_keyword(), "SCHEMA");
1232    }
1233
1234    /// The default transaction answer is the ANSI one: a load can be wrapped,
1235    /// in the standard spelling, and clearing a staging table stays inside the
1236    /// wrap. Every platform that differs says so on its own dialect.
1237    #[test]
1238    fn the_default_load_wrap_is_the_ansi_one() {
1239        let d = DefaultDialect;
1240        assert!(d.supports_multi_statement_transactions());
1241        assert_eq!(d.begin_transaction_keyword(), "START TRANSACTION");
1242        assert_eq!(d.commit_transaction_keyword(), "COMMIT");
1243        assert!(!d.truncate_commits_implicitly());
1244        assert!(!d.begin_leaves_the_commit_to_the_client());
1245    }
1246
1247    /// **A bound on the digits before the point is its own refusal**, and the
1248    /// nearest type that fits keeps the scale and gives up whole digits.
1249    #[test]
1250    fn digits_before_the_point_have_their_own_bound() {
1251        let bignumeric = DecimalLimits {
1252            max_precision: 76,
1253            max_scale: 38,
1254            scale_within_precision: true,
1255            max_integer_digits: Some(38),
1256        };
1257        assert_eq!(bignumeric.misfit(38, 0), None);
1258        assert_eq!(bignumeric.misfit(76, 38), None);
1259        assert_eq!(
1260            bignumeric.misfit(39, 0).as_deref(),
1261            Some("its decimals hold at most 38 digits before the point, and this asks for 39")
1262        );
1263        assert_eq!(bignumeric.nearest(39, 0), (38, 0));
1264        assert_eq!(bignumeric.nearest(60, 10), (48, 10));
1265        assert_eq!(bignumeric.nearest(99, 40), (76, 38));
1266        for (precision, scale) in [(39, 0), (60, 10), (99, 40), (5, 6), (0, 0)] {
1267            let (p, s) = bignumeric.nearest(precision, scale);
1268            assert_eq!(bignumeric.misfit(p, s), None, "nearest({precision}, {scale}) fits");
1269        }
1270
1271        let unbounded = DecimalLimits { max_integer_digits: None, ..bignumeric };
1272        assert_eq!(unbounded.misfit(76, 0), None, "the digits in all are the only bound");
1273    }
1274}