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