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