Skip to main content

razeri_plugin_contract/
renderer.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//! Layered rendering traits for SQL generation.
6//!
7//! The hierarchy is designed so that implementing [`SqlRenderer`] (4 methods)
8//! gives you ANSI-standard defaults for all higher-level traits.
9
10use crate::DataType;
11use crate::constraint_enforcement::ConstraintEnforcement;
12use crate::date_format::DateFormat;
13use crate::evolution::ColumnEvolution;
14use crate::flatten::{Flatten, FlattenRequest};
15use crate::number_format::NumberFormat;
16use crate::procedure_log_level::ProcedureLogLevel;
17use crate::routine_form::{ProcedureForm, ScalarFunctionForm, TableFunctionForm};
18use crate::routine_spec::RoutineSpec;
19use crate::sid_spelling::{SidHelperBody, SidSpelling, UNSPELLED_SID};
20
21/// **Where the aggregated expression goes** in a form returned by
22/// [`SqlRenderer::median_aggregate_form`].
23///
24/// The form is a template rather than finished SQL so that *which function*
25/// and *how a column is written* stay in different hands — the dialect answers
26/// the first, the query renderer the second. A form must carry this token
27/// exactly once.
28pub const AGGREGATE_ARGUMENT: &str = "{argument}";
29
30/// **What kind of value an aggregate would have to collapse** — the axis
31/// [`SqlRenderer::render_single_row_aggregate`] varies on, held to three arms
32/// because three is what the measurements distinguish.
33///
34/// Not the full type lattice: `MAX` behaves identically across every numeric,
35/// textual and temporal type on every platform razeri reaches, and splitting
36/// them would invite per-type arms nothing has measured.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum AggregableKind {
39    /// Numeric, textual and temporal — every platform orders these and every
40    /// platform has `MAX` for them.
41    Orderable,
42    /// A truth value. `PostgreSQL` and SQL Server have no `MAX` for it and each
43    /// spells the collapse differently.
44    Boolean,
45    /// `uuid`, JSON, binary, interval, array, map — the families with no
46    /// ordering to borrow. Availability is per platform and is measured, never
47    /// assumed.
48    Opaque,
49}
50
51/// **A clock bound once for a whole load script** — what
52/// [`SqlRenderer::render_inline_clock_binding`] answers on an engine whose
53/// loads ship as flat statement lists rather than procedure bodies.
54///
55/// The `statements` run first and capture the engine's current moment under
56/// one name; `reference` is the expression every later statement of the same
57/// run reads it back by. One binding, many readers, is what makes a version's
58/// close and its successor's open the same instant by construction.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct InlineClockBinding {
61    /// The statement(s) that bind the moment, in run order, each terminated.
62    pub statements: Vec<String>,
63    /// The expression that reads the bound moment back.
64    pub reference: String,
65}
66
67/// **One delimited source file, in every spelling an engine's file relation may
68/// ask for it in** — what
69/// [`SqlRenderer::delimited_file_relation_for`] is handed.
70///
71/// One struct rather than four arguments, because a slot that grows an argument
72/// is a slot every dialect has to be recompiled for; one that grows a field is
73/// not. Each is composed once, by the caller that knows the answer, so no
74/// dialect has to take a path apart to find a file name in it.
75#[derive(Debug, Clone, Copy)]
76pub struct DelimitedFile<'a> {
77    /// The whole path to the file, already a single-quoted SQL string literal
78    /// and already escaped. Put it where the relation takes a path, and do not
79    /// quote it again.
80    pub path_literal: &'a str,
81    /// The directory the file sits in, already a single-quoted SQL string
82    /// literal — for an engine that resolves a file name against a directory
83    /// rather than opening a path.
84    pub directory_literal: &'a str,
85    /// The file's own name inside that directory, bare: no quotes and no path
86    /// in front of it.
87    pub name: &'a str,
88    /// **The file's columns, in the order the file writes them**, spelled
89    /// exactly as its header row spells them and with no quoting applied.
90    ///
91    /// Empty where the caller measured no header — a bundle assembled by hand.
92    /// An engine that cannot read a file without its columns declared answers
93    /// `None` to an empty list rather than guessing at one.
94    pub columns: &'a [String],
95}
96
97/// Level 1 — Core SQL rendering. REQUIRED: every dialect must implement these 4 methods.
98///
99/// These methods define the absolute minimum surface for rendering SQL:
100/// data types, identifier quoting, qualified names, and timestamps.
101pub trait SqlRenderer: Send + Sync {
102    /// Render a [`DataType`] as platform-specific SQL.
103    fn render_data_type(&self, dt: &DataType) -> String;
104
105    /// **The type text for a routine's PARAMETER**, which is not always the text
106    /// a column carries.
107    ///
108    /// Most engines spell a type one way wherever it appears, and for those this
109    /// hook is [`Self::render_data_type`] under another name — which is the
110    /// default, so a dialect with nothing to say says nothing.
111    ///
112    /// **Oracle is the reason it exists.** A column there takes
113    /// `NUMBER(19, 0)` and `VARCHAR2(100)`; the same types in a parameter list
114    /// are a compile error (`PLS-00103`, reported at the opening parenthesis),
115    /// and the bare `NUMBER` and `VARCHAR2` are what a parameter clause takes.
116    /// The length and the precision belong on the local declaration instead,
117    /// which keeps them. A dialect there answers:
118    ///
119    /// ```ignore
120    /// fn render_parameter_data_type(&self, dt: &DataType) -> String {
121    ///     let column = self.render_data_type(dt);
122    ///     match column.find('(') {
123    ///         Some(at) => column[..at].trim_end().to_owned(),
124    ///         None => column,
125    ///     }
126    /// }
127    /// ```
128    ///
129    /// It is what fills
130    /// [`RoutineParameter::data_type`](crate::routine_spec::RoutineParameter::data_type),
131    /// so a dialect that answers it does not also post-process the string
132    /// Razeri handed it.
133    ///
134    /// Parameters:
135    /// - `dt`: the type the design declared.
136    fn render_parameter_data_type(&self, dt: &DataType) -> String {
137        self.render_data_type(dt)
138    }
139
140    /// **The type text for a column that ADMITS NULL** — the whole type, not a
141    /// modifier appended to one.
142    ///
143    /// Nine of razeri's ten platforms spell nullability by SILENCE: a column
144    /// declared without `NOT NULL` admits one, and its type text is the same
145    /// text a required column carries. That is the default here, and for those
146    /// nine this hook is [`SqlRenderer::render_data_type`] under another name.
147    ///
148    /// **`ClickHouse` is the exception, and it is the reason this hook exists.**
149    /// There a bare type is NOT NULL — nullability is a property OF the type,
150    /// spelled `Nullable(T)` — so a renderer that expresses *nullable* by
151    /// omitting a keyword expresses nothing at all. `render_field` used to carry
152    /// a comment saying the wrapper was *"handled in data type"*; it was handled
153    /// nowhere, and every `nullable:` a design declared was discarded on that
154    /// one engine (`Nullable(` appeared ZERO times in any rendered
155    /// `schema.sql`).
156    ///
157    /// # What the discard actually cost, measured rather than assumed
158    ///
159    /// Not a loud refusal. `insert_null_as_default` **defaults to `1`** on
160    /// `ClickHouse` (24.10.2, server default, `changed = 0`), so
161    /// `INSERT … SELECT NULL` into a bare column does not raise — it writes the
162    /// TYPE'S DEFAULT, silently: `''` for `String`, `0` for `Int64`,
163    /// `1900-01-01` for `Date32` (which is razeri's own unknown-member date, so
164    /// an unknown attribute came back indistinguishable from a deliberately
165    /// seeded one). Only with `insert_null_as_default = 0` does the engine
166    /// answer `Code: 70 … Cannot convert NULL to a non-nullable type`. **A
167    /// design's own data was being fabricated on one engine and honest on the
168    /// other nine**, and the DDL is where that is fixed.
169    ///
170    /// # A dialect overriding this owes a measurement
171    ///
172    /// Not every type can be wrapped. `ClickHouse` refuses
173    /// `Nullable(Array(…))`, `Nullable(Map(…))` and `Nullable(JSON)` outright,
174    /// so its override declines those and says so in its own rustdoc. A
175    /// wrapper that cannot be applied must be DECLINED by name, never applied
176    /// and hoped for: an unwrappable type rendered `Nullable(…)` turns a silent
177    /// wrong value into a `CREATE TABLE` that does not run.
178    fn render_nullable_data_type(&self, dt: &DataType) -> String {
179        self.render_data_type(dt)
180    }
181
182    /// Quote an identifier using platform conventions.
183    ///
184    /// `PostgreSQL`: `"name"`, `MySQL`: `` `name` ``, SQL Server: `[name]`
185    fn quote_identifier(&self, name: &str) -> String;
186
187    /// Render a fully qualified name (optional schema + name).
188    fn qualify_name(&self, schema: Option<&str>, name: &str) -> String;
189
190    /// Whether this dialect uses schemas in qualified names.
191    ///
192    /// `ClickHouse` uses only `database.object` (no schema level).
193    /// Default: `true` (most platforms use `catalog.schema.object`).
194    fn uses_schema_in_qualified_names(&self) -> bool {
195        true
196    }
197
198    /// Whether this dialect is the "agnostic" pseudo-dialect.
199    ///
200    /// Agnostic rendering produces unquoted, platform-neutral DDL.
201    /// Real dialect implementations should not override this.
202    fn is_agnostic_dialect(&self) -> bool {
203        false
204    }
205
206    /// Whether this dialect supports `SECURE VIEW` (Snowflake).
207    fn supports_secure_view(&self) -> bool {
208        false
209    }
210
211    /// Whether this dialect supports `GLOBAL TEMPORARY VIEW` (Databricks).
212    fn supports_global_temporary_view(&self) -> bool {
213        false
214    }
215
216    /// Render the platform's `CURRENT_TIMESTAMP` expression.
217    fn render_current_timestamp(&self) -> String;
218
219    /// **The schemas a catalog scan passes over** — the engine's own
220    /// bookkeeping, never a warehouse's contents.
221    ///
222    /// Read by [`Self::render_catalog_columns_query`] and
223    /// [`Self::render_catalog_object_times_query`] when no single schema was
224    /// named, so a scan of a whole warehouse sees the tables somebody created
225    /// and not the catalog describing them. The default names the one schema
226    /// the SQL standard puts the catalog in; an engine that keeps more of its
227    /// own next to it adds them.
228    fn catalog_system_schemas(&self) -> &'static [&'static str] {
229        &["information_schema"]
230    }
231
232    /// **The query that lists what the warehouse holds** — one row per column,
233    /// as `(schema, object, column)`.
234    ///
235    /// Razeri asks it to answer a question it cannot answer from a design: how
236    /// much of this warehouse razeri itself rendered. Columns rather than
237    /// object names alone, because the answer is read off both — a name carries
238    /// the tier razeri mints (`…_staging`, `…_current`), and a column carries
239    /// the bookkeeping only razeri writes (`…_sid`, `_loaded_at`). A scan that
240    /// saw names alone would either miss an entity whose target table is a
241    /// plain noun or count a table razeri never touched.
242    ///
243    /// The three columns are positional and in that order: every caller reads
244    /// row `0`, `1`, `2` and nothing reads a column by name, so an engine whose
245    /// catalog spells the headings differently needs no special case.
246    ///
247    /// `None` means **this engine states no catalog razeri can scan**, which is
248    /// an answer rather than an omission: the caller says so by name instead of
249    /// reporting an empty warehouse.
250    ///
251    /// Parameters:
252    /// - `schema`: the one schema to look in. `None` means the whole warehouse,
253    ///   minus [`Self::catalog_system_schemas`].
254    fn render_catalog_columns_query(&self, schema: Option<&str>) -> Option<String> {
255        Some(format!(
256            "SELECT table_schema, table_name, column_name\nFROM \
257             information_schema.columns\n{}\nORDER BY table_schema, table_name, column_name",
258            self.catalog_scope_predicate("table_schema", schema)
259        ))
260    }
261
262    /// **The query that says when each object was created**, as
263    /// `(schema, object, created_at)` — where the engine records it at all.
264    ///
265    /// A second, independent reading of the same question
266    /// [`Self::render_catalog_columns_query`] opens: the earliest moment razeri
267    /// can prove it was here. It is a `None` on most engines, and deliberately
268    /// so — `PostgreSQL` and `DuckDB` keep no creation time for a relation, and
269    /// inventing one would be worse than having none.
270    ///
271    /// Parameters:
272    /// - `schema`: the one schema to look in, or `None` for the whole
273    ///   warehouse.
274    fn render_catalog_object_times_query(&self, schema: Option<&str>) -> Option<String> {
275        let _ = schema;
276        None
277    }
278
279    /// The `WHERE` clause both catalog queries share: one named schema, or
280    /// every schema that is not the engine's own.
281    ///
282    /// Parameters:
283    /// - `column`: the catalog's own name for the schema column.
284    /// - `schema`: the one schema to look in, or `None` for the whole
285    ///   warehouse.
286    fn catalog_scope_predicate(&self, column: &str, schema: Option<&str>) -> String {
287        if let Some(one) = schema {
288            return format!("WHERE {column} = '{}'", one.replace('\'', "''"));
289        }
290        let excluded: Vec<String> = self
291            .catalog_system_schemas()
292            .iter()
293            .map(|name| format!("'{}'", name.replace('\'', "''")))
294            .collect();
295        format!("WHERE {column} NOT IN ({})", excluded.join(", "))
296    }
297
298    /// Return a stable string identifier for this dialect.
299    ///
300    /// Used for dispatch when trait-based rendering is not yet available for
301    /// a domain concept, and as a human-readable label in diagnostics and
302    /// error messages.  This method works for **all dialects** registered
303    /// in the `PlatformRegistry` (e.g. `"postgresql"`, `"sqlite"`,
304    /// `"cockroachdb"`).
305    ///
306    /// Convention: lowercase, hyphen-separated (e.g. `"postgresql"`,
307    /// `"sql-server"`, `"snowflake"`, `"sqlite"`).
308    ///
309    /// The default returns `"ansi"`.
310    fn dialect_name(&self) -> &'static str {
311        "ansi"
312    }
313
314    /// **How this engine spells a surrogate key.**
315    ///
316    /// Razeri derives every key one way — normalize each part, join them, hash
317    /// the result, guard NULLs with a sentinel — and composes that derivation
318    /// in exactly one place. What a dialect decides is the *vocabulary*: what
319    /// this engine calls `LOWER` and `TRIM`, how it joins strings, how it casts
320    /// a value to text, and how it spells a 64-bit hash.
321    ///
322    /// The default is [`UNSPELLED_SID`] — ANSI's vocabulary under a name that
323    /// says it is a guess. **A dialect that leaves it there is telling Razeri it
324    /// has not decided**, and the emitted SQL will call an `xxhash64` its engine
325    /// very likely does not have. Override it.
326    ///
327    /// If your engine has no portable 64-bit hash, say so rather than reaching
328    /// for the nearest-looking function: a hash that is deterministic within one
329    /// warehouse and equal to nothing outside it is the lock-in a portable key
330    /// exists to end. Name an artifact your dialect's prerequisites install, or
331    /// refuse the key and orient the reader.
332    fn sid_spelling(&self) -> SidSpelling {
333        UNSPELLED_SID
334    }
335
336    /// **The body of the surrogate-key helper, where SQL cannot spell it.**
337    ///
338    /// Razeri emits one function per warehouse — `generate_sid` — and normally
339    /// builds its body itself, from the vocabulary
340    /// [`sid_spelling`](Self::sid_spelling) states. The default here is `None`,
341    /// which says exactly that: *compose the derivation in my own SQL*.
342    ///
343    /// Answer with [`SidHelperBody`] only when there is no SQL that could hold
344    /// the derivation — the engine has no 64-bit hash under any name, so the
345    /// vocabulary has nothing to spell the hash with and the definition has to
346    /// be written in another language. `BigQuery` is the case Razeri ships:
347    /// `GoogleSQL` has no XXH64, and the function that looks like the answer
348    /// (`FARM_FINGERPRINT`) is a different number, so the body is JavaScript.
349    ///
350    /// **Do not use this to change the key.** The body computes the same number
351    /// every other engine computes — XXH64, seed 0, over the UTF-8 bytes of the
352    /// one already-normalized argument — and [`SidHelperBody`] says what it owes
353    /// in full. Reaching for a native hash that is merely deterministic keys a
354    /// warehouse to numbers nothing outside it can reproduce.
355    ///
356    /// A dialect that answers here should also state a
357    /// [`SidSpelling::hash`](crate::SidSpelling::hash) that no engine has, so
358    /// that any path which fell back to composing SQL fails loudly at deploy
359    /// time rather than hashing nothing and handing back the value.
360    fn sid_helper_body(&self) -> Option<SidHelperBody> {
361        None
362    }
363
364    /// **How this engine reads a delimited text file as a relation**, or `None`
365    /// where it cannot.
366    ///
367    /// A design can bind an entity to a source file, and Razeri then generates
368    /// the staging fill: an `INSERT … SELECT … FROM <this>`. That only works on
369    /// an engine that can open a file from inside a query. `DuckDB` can
370    /// (`read_csv_auto`); most cannot — `PostgreSQL` wants `COPY`, Snowflake a
371    /// stage, `SQLite` has nothing at all. Each of those is a bulk-load design
372    /// decision rather than a rendering detail, so the default is `None` and
373    /// the generated bundle says which platform declined and why instead of
374    /// emitting SQL the engine would refuse.
375    ///
376    /// `path_literal` arrives as a finished single-quoted SQL string literal,
377    /// already escaped — put it where the relation takes a path and do not
378    /// quote it again.
379    ///
380    /// **What the returned relation owes**, because the fill is written against
381    /// it and nothing checks these at render time:
382    ///
383    /// - the file's first row is its header, and the column names come from it,
384    ///   spelled exactly as the file spells them;
385    /// - every column reads as **text**, unparsed. The fill casts nothing: a
386    ///   date's layout and a currency's prefix are the source's business, and a
387    ///   reader that guessed a type would fail the load on its first row;
388    /// - it is a relation usable in a `FROM` clause, not a statement.
389    ///
390    /// The path is embedded in the generated file as it was spelled when the
391    /// bundle was built, so it must be one the engine resolves at run time.
392    fn delimited_file_relation(&self, path_literal: &str) -> Option<String> {
393        let _ = path_literal;
394        None
395    }
396
397    /// **How this engine reads a delimited text file as a relation, told
398    /// everything Razeri knows about the file** — or `None` where it cannot.
399    ///
400    /// The richer spelling of
401    /// [`delimited_file_relation`](Self::delimited_file_relation), and the one
402    /// the staging fill asks first. The default hands the path straight to that
403    /// method and ignores the rest, so a dialect that has stated one has stated
404    /// this too and its renders are byte for byte what they were.
405    ///
406    /// Override it where the path alone is not enough. Two engines' file reads
407    /// need more and cannot be given it any other way:
408    ///
409    /// - **the file's columns declared in the statement.** Oracle's external
410    ///   table answers `ORA-00906: missing left parenthesis` without them, and
411    ///   has no form that takes its column names from a header row;
412    /// - **a directory rather than a path.** The same engine answers
413    ///   `KUP-04125: no directory object specified for location 1 in LOCATION
414    ///   clause` to an absolute path, and resolves a file name against a named
415    ///   directory object instead — which
416    ///   [`delimited_file_prerequisites`](Self::delimited_file_prerequisites)
417    ///   is where such an object is made.
418    ///
419    /// Everything the returned relation owes, it owes for the reasons
420    /// [`delimited_file_relation`](Self::delimited_file_relation) states:
421    /// header from the first row, every column as text and unparsed, usable in
422    /// a `FROM` clause. **Declaring the columns does not license declaring
423    /// their types**: they are the source's text, and a reader that guessed
424    /// would fail the load on its first row.
425    fn delimited_file_relation_for(&self, file: &DelimitedFile<'_>) -> Option<String> {
426        self.delimited_file_relation(file.path_literal)
427    }
428
429    /// **Statements this engine needs run once before any of its file relations
430    /// resolve**, in order — and none by default.
431    ///
432    /// An engine that opens a path needs nothing here. An engine that opens a
433    /// file only through an object somebody made — Oracle's `DIRECTORY` — needs
434    /// that object made first, and the generated staging file is where it
435    /// belongs: it is the one file that knows which directory the sources were
436    /// read from.
437    ///
438    /// Each string is one statement without its terminator. Every one must be
439    /// safe to run again, because the file it lands in runs on every deploy.
440    ///
441    /// `directory_literal` arrives as a finished single-quoted SQL string
442    /// literal, already escaped.
443    ///
444    /// **Say what the statement costs.** An object like this is often
445    /// privileged, and a statement an operator cannot run is better read than
446    /// guessed at — put the privilege in a comment inside the statement, where
447    /// it travels with it.
448    fn delimited_file_prerequisites(&self, directory_literal: &str) -> Vec<String> {
449        let _ = directory_literal;
450        Vec::new()
451    }
452
453    /// **How this engine reads a delimited text file whose bytes are not
454    /// UTF-8**, or `None` where it states no way to.
455    ///
456    /// UTF-8 is the canonical form and every engine reads it, so
457    /// [`delimited_file_relation`](Self::delimited_file_relation) answers for
458    /// it and this is asked only about the other days. A design can name the
459    /// encoding to fall back to when a file's bytes are not UTF-8, and razeri
460    /// resolves that name once, to the one name the encoding standard files it
461    /// under — `windows-1252`, `shift_jis`, `koi8-r`. That name arrives here,
462    /// and an engine that has its own word for the same encoding translates it
463    /// the way an engine translates every other name razeri hands it.
464    ///
465    /// Everything the returned relation owes, it owes for the same reasons
466    /// [`delimited_file_relation`](Self::delimited_file_relation) says: header
467    /// from the first row, every column as text, usable in a `FROM` clause.
468    ///
469    /// **`None` is a complete answer, and the common one.** An engine that
470    /// reads no file at all says nothing here; so does an engine that reads
471    /// files but has no word for this particular encoding. Razeri then declines
472    /// that source by name and says what to do instead — convert the file to
473    /// UTF-8, or write the load by hand — rather than pointing the engine at
474    /// bytes it will read as some other language.
475    ///
476    /// **Answer only for encodings you have run.** An engine that accepts the
477    /// name and reads the bytes as something else is worse than one that
478    /// refuses the name: the first is a warehouse of plausible nonsense and the
479    /// second is a sentence on a terminal.
480    ///
481    /// `path_literal` arrives as a finished single-quoted SQL string literal,
482    /// already escaped — put it where the relation takes a path and do not
483    /// quote it again. `encoding` does not: it is a bare name, and an engine
484    /// that puts it in a string literal writes the quotes itself.
485    fn delimited_file_relation_in_encoding(
486        &self,
487        path_literal: &str,
488        encoding: &str,
489    ) -> Option<String> {
490        let _ = (path_literal, encoding);
491        None
492    }
493
494    /// **The `CAST` target spelling for a [`DataType`] on this engine.**
495    ///
496    /// A column declaration and a `CAST` target are two grammars on most
497    /// engines, so this is a separate answer from
498    /// [`render_data_type`](Self::render_data_type) and not a synonym for it.
499    /// `MySQL` takes `SIGNED` where it declares `BIGINT`; T-SQL takes
500    /// `NVARCHAR(MAX)` where ANSI says `TEXT`.
501    ///
502    /// Where the two *are* the same on your engine — which is the common case
503    /// on engines with one type vocabulary — say so by delegating:
504    ///
505    /// ```ignore
506    /// fn render_cast_type(&self, dt: &DataType) -> String {
507    ///     self.render_data_type(dt)
508    /// }
509    /// ```
510    ///
511    /// The default is ANSI's cast vocabulary. Razeri's generated load opens
512    /// with a CTE that casts every source column to its declared type, so a
513    /// wrong name here fails the load on its first statement.
514    fn render_cast_type(&self, dt: &DataType) -> String {
515        crate::agnostic::render_cast_type_ansi(dt)
516    }
517
518    /// **Which stored-procedure grammar this engine speaks.**
519    ///
520    /// Razeri composes the procedure — its parameters, its steps, its comment —
521    /// and spells it in the form this returns. The default is
522    /// [`ProcedureForm::Agnostic`], which parses on no engine: it is the honest
523    /// answer for a dialect that has not said, and it is what an unlisted
524    /// platform has always been handed.
525    ///
526    /// It lives on `SqlRenderer` rather than on
527    /// [`ProcedureRenderer`] because the composer
528    /// holds the renderer as a [`DdlRenderer`], and a decision reachable only
529    /// through a trait the caller does not hold is a decision the caller makes
530    /// instead.
531    fn procedure_form(&self) -> ProcedureForm {
532        ProcedureForm::Agnostic
533    }
534
535    /// **Which scalar-function grammar this engine speaks.**
536    ///
537    /// The default is [`ScalarFunctionForm::PostgreSql`] — what an unlisted
538    /// dialect has always been handed, and a dangerous inheritance on a near
539    /// neighbour: `LANGUAGE SQL IMMUTABLE PARALLEL SAFE` is a *"syntax error at
540    /// or near `PARALLEL`"* on an engine that descends from `PostgreSQL` and
541    /// reports itself as one.
542    fn scalar_function_form(&self) -> ScalarFunctionForm {
543        ScalarFunctionForm::PostgreSql
544    }
545
546    /// **Which table-valued-function grammar this engine speaks, and — where it
547    /// has none — the words it refuses in.**
548    ///
549    /// The default is [`TableFunctionForm::PostgreSql`], what an unlisted
550    /// dialect has always been handed. If your engine has no routine a query
551    /// selects `FROM`, answer [`TableFunctionForm::Unsupported`] with your own
552    /// sentence: Razeri then declines to build the routine at all and prints
553    /// **your** words rather than another engine's. A refusal written once per
554    /// artifact kind tells every author about whichever engine happened to need
555    /// it first, and the near form an author will otherwise find on their own
556    /// is exactly what costs them a day.
557    fn table_function_form(&self) -> TableFunctionForm {
558        TableFunctionForm::PostgreSql
559    }
560
561    // ═══════════════════════════════════════════════════════════════
562    // Routines this dialect spells itself
563    // ═══════════════════════════════════════════════════════════════
564
565    /// **Spell a whole stored procedure in this engine's own grammar** — the
566    /// method [`ProcedureForm::Custom`] promises.
567    ///
568    /// Razeri composes the routine and hands it over as a [`RoutineSpec`]: the
569    /// name, the parameters with their types already in your spelling, the
570    /// declarations, the body's statements, the comment. What comes back is the
571    /// complete `CREATE`, punctuation and all, and Razeri emits it unchanged.
572    ///
573    /// The default is `None`, which means *"I did not write one"*. It is only
574    /// ever reached by a dialect that answered
575    /// [`ProcedureForm::Custom`] and then did not override this — the
576    /// conformance kit reports that as blocking, and the emitted SQL carries a
577    /// comment saying the same thing where the procedure would have been.
578    ///
579    /// It lives on `SqlRenderer` rather than on [`ProcedureRenderer`] for the
580    /// reason [`Self::procedure_form`] does: the composer holds the renderer as
581    /// a [`DdlRenderer`], and an answer reachable only through a trait the
582    /// caller does not hold is an answer the caller makes up instead.
583    ///
584    /// Parameters:
585    /// - `routine`: the procedure Razeri composed.
586    fn render_procedure(&self, routine: &RoutineSpec) -> Option<String> {
587        let _ = routine;
588        None
589    }
590
591    /// **Spell a whole scalar function in this engine's own grammar** — the
592    /// method [`ScalarFunctionForm::Custom`] promises.
593    ///
594    /// The same contract as [`Self::render_procedure`]: a composed
595    /// [`RoutineSpec`] in, the complete `CREATE` out, and `None` only where a
596    /// dialect claimed a custom grammar and did not write the method.
597    ///
598    /// Parameters:
599    /// - `routine`: the function Razeri composed.
600    fn render_scalar_function(&self, routine: &RoutineSpec) -> Option<String> {
601        let _ = routine;
602        None
603    }
604
605    /// **Spell a whole table-valued function in this engine's own grammar** —
606    /// the method [`TableFunctionForm::Custom`] promises.
607    ///
608    /// The same contract as [`Self::render_procedure`]: a composed
609    /// [`RoutineSpec`] in, the complete `CREATE` out, and `None` only where a
610    /// dialect claimed a custom grammar and did not write the method.
611    ///
612    /// Parameters:
613    /// - `routine`: the function Razeri composed.
614    fn render_table_function(&self, routine: &RoutineSpec) -> Option<String> {
615        let _ = routine;
616        None
617    }
618
619    // ═══════════════════════════════════════════════════════════════
620    // Expression rendering extensions
621    // ═══════════════════════════════════════════════════════════════
622
623    /// Platform's `CURRENT_DATE` expression.
624    ///
625    /// `SqlServer`: `CAST(GETDATE() AS DATE)`, `ClickHouse`: `today()`, others: `CURRENT_DATE`
626    fn render_current_date(&self) -> String {
627        "CURRENT_DATE".to_string()
628    }
629
630    /// Render a boolean literal.
631    ///
632    /// SqlServer/ClickHouse: `1`/`0`, others: `TRUE`/`FALSE`
633    fn render_boolean_literal(&self, value: bool) -> String {
634        if value { "TRUE".to_string() } else { "FALSE".to_string() }
635    }
636
637    /// **Collapse a subquery that selects AT MOST ONE ROW into an aggregate**,
638    /// for the engines that will not accept an unaggregated correlated scalar.
639    ///
640    /// Razeri's SCD-2 and SCD-1 re-opens assign `col = (SELECT … WHERE
641    /// {correlation})`. Databricks refuses that shape by name
642    /// (`MUST_AGGREGATE_CORRELATED_SCALAR_SUBQUERY`) and Snowflake stopped
643    /// unnesting it the day the guard beside it became uncorrelated, so the
644    /// emission wraps the projection in an aggregate. **Which** aggregate is a
645    /// dialect fact and **whether one exists at all** is a dialect × type fact,
646    /// which is why this hook takes the kind and may answer `None`:
647    ///
648    /// - `MAX` is the obvious choice and is wrong for a BOOLEAN on two of the
649    ///   platforms razeri reaches. Measured on the running
650    ///   containers: `PostgreSQL` and Citus raise
651    ///   *"function max(boolean) does not exist"*, SQL Server raises
652    ///   *"Msg 8117: Operand data type bit is invalid for max operator"*, while
653    ///   `MariaDB` (`MAX(TRUE)` → `1`) and `ClickHouse` (`max(true)` → `true`)
654    ///   answer it happily;
655    /// - `PostgreSQL` has no `max()` for `uuid`, `jsonb` or `bytea` either
656    ///   (measured, same run), and SQL Server has one for both
657    ///   `uniqueidentifier` and `varbinary` (measured).
658    ///
659    /// **`None` means *this dialect cannot aggregate this kind* and is not a
660    /// failure.** The caller then emits the bare correlated scalar — the
661    /// prior shape, measured green on the whole
662    /// `PostgreSQL` family — and says so on the output, because that shape is
663    /// the one Databricks refuses.
664    ///
665    /// **Correctness does not come from the aggregate's meaning.** The subquery
666    /// is under a guard that is false for the entire load if any member
667    /// disagrees with itself, so it selects one row; `MAX`, `BOOL_OR` and
668    /// `BOOLOR_AGG` of one value are all that value. The choice between them is
669    /// availability, not semantics.
670    fn render_single_row_aggregate(
671        &self,
672        expression: &str,
673        kind: AggregableKind,
674    ) -> Option<String> {
675        let _ = kind;
676        Some(format!("MAX({expression})"))
677    }
678
679    /// **Take any one value of `expression` from a group**, for the engines
680    /// that cannot group on the column itself.
681    ///
682    /// A load collapses staged rows that repeat. Where the engine will not
683    /// group on a nested column
684    /// ([`crate::DialectCapabilities::groups_nested_values`]) the collapse
685    /// groups on the remaining columns and takes one value of that one, which
686    /// is what this renders.
687    ///
688    /// `ANY_VALUE` is the spelling `BigQuery`, Snowflake, Redshift, Databricks
689    /// and `MySQL` all use, so it is the default; an engine that spells it
690    /// differently, or reaches it through `FIRST`, says so here.
691    ///
692    /// Unlike [`Self::render_single_row_aggregate`] this one cannot decline:
693    /// it is asked only where the dialect has already said it cannot group on
694    /// the column, so there is no second shape to fall back to.
695    ///
696    /// Parameters:
697    /// - `expression`: the column reference, already spelled by the renderer
698    ///   that lays out the query.
699    fn render_any_value(&self, expression: &str) -> String {
700        format!("ANY_VALUE({expression})")
701    }
702
703    /// **The AGGREGATE median this engine has for `expression`, or `None` when
704    /// it has none.**
705    ///
706    /// Aggregate is the load-bearing word, and it is why this cannot be
707    /// answered from a function-name table. Four spellings are in the wild:
708    ///
709    /// - a plain `median(x)` aggregate — `duckdb`, `clickhouse`, `snowflake`;
710    /// - the SQL ordered-set aggregate
711    ///   `PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY x)` — `postgres` and its
712    ///   family (measured on a running instance: it groups);
713    /// - a two-argument `PERCENTILE(x, 0.5)` — `databricks`;
714    /// - and a WINDOW function wearing one of those names, which is **not**
715    ///   this. `mariadb` has `MEDIAN(x) OVER (…)` and `sqlserver` has
716    ///   `PERCENTILE_CONT(…) OVER (…)`, and both are a syntax error in a
717    ///   `GROUP BY` select list (measured: `MariaDB` raises `ERROR 1064` and
718    ///   SQL Server raises *"Msg 10753: The function `PERCENTILE_CONT` must
719    ///   have an OVER clause"*). A window function returns one value per ROW,
720    ///   not one per group, so it is a different answer and not a fallback.
721    ///
722    /// **The default declines**, so a dialect that has never been asked the
723    /// question is never assumed to have the answer, and a dialect added
724    /// tomorrow opts in deliberately with a form somebody measured. The caller
725    /// turns `None` into a build-time refusal naming the column and the target
726    /// — razeri does not emit SQL its target cannot run.
727    ///
728    /// # Why a TEMPLATE rather than finished SQL
729    ///
730    /// The aggregated expression is a column reference, and how a column
731    /// reference is spelled — quoted, bracketed, upper-cased — belongs to the
732    /// renderer that lays out the query, not to this answer. Handing back a
733    /// form with [`AGGREGATE_ARGUMENT`] where the expression goes keeps the
734    /// two apart: this trait owns *which function*, the query renderer owns
735    /// *how the column is written*, and neither has to reimplement the other.
736    fn median_aggregate_form(&self) -> Option<&'static str> {
737        None
738    }
739
740    /// Render a date literal.
741    ///
742    /// `ClickHouse`: `toDate('2024-01-15')`, others: `DATE '2024-01-15'`
743    fn render_date_literal(&self, date: &str) -> String {
744        format!("DATE '{date}'")
745    }
746
747    /// Render a timestamp literal.
748    ///
749    /// `ClickHouse`: `toDateTime('...')`, others: `TIMESTAMP '...'`
750    fn render_timestamp_literal(&self, ts: &str) -> String {
751        format!("TIMESTAMP '{ts}'")
752    }
753
754    /// Render an interval literal.
755    ///
756    /// `SqlServer`: `'P1Y2M3D'`, `ClickHouse`: `INTERVAL 1 YEAR`, others: `INTERVAL '1 year'`
757    fn render_interval_literal(&self, interval: &str) -> String {
758        format!("INTERVAL '{interval}'")
759    }
760
761    /// Render the string concatenation operator.
762    ///
763    /// `SqlServer`: `+`, others: `||`
764    fn concat_operator(&self) -> &'static str {
765        "||"
766    }
767
768    /// Render a NaN literal for a floating-point type.
769    ///
770    /// `PostgreSQL`: `'NaN'::float`, others: `NULL`
771    fn render_nan_literal(&self, _dt: &DataType) -> String {
772        "NULL".to_string()
773    }
774
775    /// Render an infinity literal for a floating-point type.
776    ///
777    /// `PostgreSQL`: `'Infinity'::float` / `'-Infinity'::float`, others: `NULL`
778    fn render_infinity_literal(&self, _positive: bool, _dt: &DataType) -> String {
779        "NULL".to_string()
780    }
781
782    /// Render the NULL-coalescing function name.
783    ///
784    /// `SqlServer`: `ISNULL`, `ClickHouse`: `ifNull`, others: `COALESCE`
785    fn ifnull_function_name(&self) -> &'static str {
786        "COALESCE"
787    }
788
789    /// Render an IIF expression (or CASE WHEN equivalent).
790    ///
791    /// `SqlServer`: `IIF(cond, then, else)`, others: `CASE WHEN cond THEN then ELSE else END`
792    fn render_iif(&self, cond: &str, then_val: &str, else_val: &str) -> String {
793        format!("CASE WHEN {cond} THEN {then_val} ELSE {else_val} END")
794    }
795
796    /// Render a `DATE_TRUNC` expression.
797    ///
798    /// `SqlServer`: `DATETRUNC(field, expr)`, `ClickHouse`: `toStartOfMonth(expr)`, others: `DATE_TRUNC('field', expr)`
799    fn render_date_trunc(&self, field: &str, expr: &str) -> String {
800        format!("DATE_TRUNC('{field}', {expr})")
801    }
802
803    /// Render a date field extraction expression.
804    ///
805    /// `SqlServer`: `DATEPART(field, column)`, `ClickHouse`: `toYear(col)/toMonth(col)`, others: `EXTRACT(field FROM column)`
806    fn render_date_extract(&self, field: &str, column: &str) -> String {
807        format!("EXTRACT({field} FROM {column})")
808    }
809
810    /// Render a `TRY_CAST` expression.
811    ///
812    /// `ClickHouse`: `accurateCastOrNull(expr, 'Type')`, others: `TRY_CAST(expr AS Type)`
813    fn render_try_cast(&self, expr: &str, target: &DataType) -> String {
814        format!("TRY_CAST({} AS {})", expr, self.render_data_type(target))
815    }
816
817    /// Render a positional parameter placeholder.
818    ///
819    /// `PostgreSQL`: `$1`, `SqlServer`: `@p1`, Databricks: `{param1}`, others: `?`
820    fn render_parameter_placeholder(&self, index: usize) -> String {
821        format!("${index}")
822    }
823
824    /// Render a named parameter placeholder.
825    ///
826    /// `SqlServer`: `@name`, `PostgreSQL`: `:name`, Databricks: `{name}`
827    fn render_named_parameter(&self, name: &str) -> String {
828        format!(":{name}")
829    }
830
831    /// Keyword emitted for MERGE NOT MATCHED branch.
832    ///
833    /// SqlServer/Databricks: `WHEN NOT MATCHED BY TARGET`, others: `WHEN NOT MATCHED`.
834    fn merge_not_matched_keyword(&self) -> &'static str {
835        "WHEN NOT MATCHED"
836    }
837
838    /// Whether MERGE supports `WHEN NOT MATCHED BY SOURCE`.
839    fn supports_merge_not_matched_by_source(&self) -> bool {
840        false
841    }
842
843    /// Whether this platform takes a `MERGE` statement.
844    ///
845    /// **This is `DialectCapabilities::merge_shape().merges()`, restated where
846    /// only a `SqlRenderer` is in hand** — the standalone `MergeStatement`
847    /// builder takes this trait alone and cannot ask the capability surface.
848    /// The two must answer identically; the capability/renderer consistency
849    /// gate holds them to equality with no exceptions, so a dialect that
850    /// overrides one and not the other is refused before it ships. An engine
851    /// whose shape is an upsert statement, an engine-deduplicated insert, or a
852    /// refusal answers `false` here.
853    fn merge_supported(&self) -> bool {
854        true
855    }
856
857    /// Whether `WITH SCHEMA EVOLUTION` is supported for MERGE.
858    fn merge_supports_schema_evolution_keyword(&self) -> bool {
859        false
860    }
861
862    /// Whether table hints are supported in MERGE target.
863    fn merge_supports_table_hints(&self) -> bool {
864        false
865    }
866
867    /// Whether OUTPUT clause should be used for MERGE result projection.
868    fn merge_uses_output_clause(&self) -> bool {
869        false
870    }
871
872    /// Whether RETURNING clause is supported for MERGE.
873    fn merge_supports_returning_clause(&self) -> bool {
874        false
875    }
876
877    /// **How this engine writes a short name for a relation.**
878    ///
879    /// A composed load gives a table, a view, a common table expression or a
880    /// subquery a short name and then addresses it by that name for the rest
881    /// of the statement. Most engines take the word `AS` in front of it, and
882    /// most of those also take the name on its own. One takes only the name:
883    /// `FROM orders AS src` is refused outright, in a `FROM` and again in a
884    /// `MERGE`.
885    ///
886    /// So the two words are the dialect's to write. `relation` arrives already
887    /// quoted and qualified; `alias` arrives already quoted. Nothing here
888    /// changes what the statement means — only how this engine spells it.
889    fn render_relation_alias(&self, relation: &str, alias: &str) -> String {
890        format!("{relation} AS {alias}")
891    }
892
893    /// **How a statement that changes the shape of the warehouse is written
894    /// inside a routine's body.**
895    ///
896    /// A generated load empties its landing table when it is finished, and on
897    /// most engines that statement stands in the body exactly as it would
898    /// stand on its own. One engine runs no such statement directly in a
899    /// block: `TRUNCATE TABLE …` there is a parse error, and the same words
900    /// handed back to the SQL engine as a string run. `statement` arrives
901    /// terminated the way the rest of the body is.
902    fn render_ddl_within_routine_body(&self, statement: &str) -> String {
903        statement.to_owned()
904    }
905
906    /// **Whether this engine lets a `MERGE` target carry an alias.**
907    ///
908    /// Most do, and the composed load addresses the target through a short
909    /// `tgt`. An engine that answers `false` gets the target's own qualified
910    /// name in every position the alias would have stood — dropping the
911    /// qualifier instead makes the shared column names ambiguous.
912    fn merge_target_accepts_alias(&self) -> bool {
913        true
914    }
915
916    /// **How this engine writes the condition a `MERGE` matches rows on.**
917    ///
918    /// Most take the condition after the word `ON` and nothing else. One
919    /// requires it in parentheses and answers `ORA-00969: missing ON keyword`
920    /// without them — pointing at the condition rather than at the word, which
921    /// is there.
922    ///
923    /// `condition` is the whole predicate, which may run over several lines.
924    fn render_merge_on(&self, condition: &str) -> String {
925        format!("ON {condition}")
926    }
927
928    /// **Whether this engine refuses a `WITH` clause in front of `MERGE`.**
929    ///
930    /// The composed load types its staging rows in one common table expression
931    /// and hashes them in a second. Where a leading `WITH` parses, the two sit
932    /// in front of the statement and the source is the second one by name.
933    /// Where it does not, the same two move inside the `USING` subquery, which
934    /// is the same query in a position the grammar accepts.
935    ///
936    /// Answer `true` if `WITH … MERGE INTO …` is a syntax error on your engine.
937    fn merge_source_requires_inlined_ctes(&self) -> bool {
938        false
939    }
940
941    /// **Whether a `WITH` clause parses in front of this engine's `INSERT`.**
942    ///
943    /// A load that writes rows without matching them — one slice replaced, or
944    /// rows appended — types its staging rows in one common table expression
945    /// and hashes them in a second, exactly as the matching load does. Most
946    /// engines take those two inside the statement, after the column list and
947    /// in front of the `SELECT`, which is where Razeri puts them by default.
948    ///
949    /// Answer `true` where the same two have to lead instead, because the
950    /// engine reads a `WITH` after a table name as something else entirely:
951    /// T-SQL parses it as a table hint and rejects the statement. Nothing here
952    /// changes the query — only where the engine will accept it written down.
953    fn insert_accepts_leading_ctes(&self) -> bool {
954        false
955    }
956
957    /// **Whether this engine can be given a function by `CREATE FUNCTION`.**
958    ///
959    /// Razeri derives a surrogate key through a small emitted function, and an
960    /// engine whose SQL surface has no `CREATE FUNCTION` at all can never
961    /// receive it — the definition has to come from application code instead.
962    /// Such an engine gets no emitted helper and no per-entity key function,
963    /// and its dialect states the substitute in its own words.
964    ///
965    /// This is about the **grammar**, not about who owns the name: an engine
966    /// that can define functions but takes its key function from an installed
967    /// artifact still answers `true` here.
968    fn supports_sql_function_ddl(&self) -> bool {
969        true
970    }
971
972    /// **Whether a scalar function's body is a bare expression.**
973    ///
974    /// Most engines want a full `SELECT` in front of the value a function
975    /// returns. Some take the expression alone and reject the `SELECT`.
976    fn scalar_function_body_is_expression(&self) -> bool {
977        false
978    }
979
980    /// **Whether a scalar function belongs to a schema.**
981    ///
982    /// On most engines a function lives in a schema and both its definition and
983    /// its call sites name it. On an engine that answers `false`, functions are
984    /// server-global: `CREATE FUNCTION` takes no qualifier, and a qualified
985    /// call names a function that does not exist.
986    fn scalar_function_names_are_schema_qualified(&self) -> bool {
987        true
988    }
989
990    /// **Whether an unqualified scalar-function call resolves.**
991    ///
992    /// Most engines resolve a bare function name through a search path. An
993    /// engine that answers `true` resolves none, so a call with no schema in
994    /// front of it has to be given one — [`DialectCapabilities::default_schema`]
995    /// is the
996    /// schema the function was created in when the caller named none.
997    ///
998    /// [`DialectCapabilities::default_schema`]: crate::DialectCapabilities::default_schema
999    fn scalar_function_call_requires_schema(&self) -> bool {
1000        false
1001    }
1002
1003    /// **How this engine invokes a stored procedure**, as a whole statement.
1004    ///
1005    /// The composed load calls its own identity step, and a deploy script that
1006    /// has no procedures at all has to recognise these calls in order to inline
1007    /// what they would have run — so the call is spelled once, here.
1008    fn render_procedure_call(&self, qualified_name: &str) -> String {
1009        format!("CALL {qualified_name}();")
1010    }
1011
1012    /// The schema separator for qualified names.
1013    ///
1014    /// Almost always `.`, but could differ for some platforms.
1015    fn schema_separator(&self) -> &'static str {
1016        "."
1017    }
1018
1019    /// The statement separator for scripts.
1020    ///
1021    /// Almost all platforms: `;\n`, `SqlServer`: `;\nGO\n`
1022    fn statement_separator(&self) -> &'static str {
1023        ";\n"
1024    }
1025
1026    // ═══════════════════════════════════════════════════════════════
1027    // UPDATE behavior hooks
1028    // ═══════════════════════════════════════════════════════════════
1029
1030    /// Whether the platform supports compound assignment operators (e.g., `col += value`).
1031    ///
1032    /// `SqlServer`: `true`, others: `false`.
1033    fn supports_compound_assignment(&self) -> bool {
1034        false
1035    }
1036
1037    /// Whether UPDATE supports FROM clause for join-based updates.
1038    ///
1039    /// `PostgreSQL`, `SqlServer`, Snowflake, `CitusDB`: `true`, others: `false`.
1040    fn supports_update_from(&self) -> bool {
1041        false
1042    }
1043
1044    /// Whether the platform uses ALTER TABLE ... UPDATE syntax (`ClickHouse` mutations).
1045    fn uses_alter_table_update(&self) -> bool {
1046        false
1047    }
1048
1049    /// **Whether a mutation on this platform can read only the row it is
1050    /// mutating.**
1051    ///
1052    /// `uses_alter_table_update` above says how the mutation is SPELLED; this
1053    /// says what the mutation may LOOK AT, and the two are independent facts
1054    /// that happen to be true of the same engine today. A platform answering
1055    /// `true` here cannot express *assign this column from another relation*
1056    /// at all: neither the `SET` list nor the `WHERE` may correlate out to a
1057    /// second table, so no re-spelling of a correlated `UPDATE` compiles —
1058    /// what is available instead is `INSERT … SELECT` over a join and a
1059    /// mutation whose predicate is an uncorrelated `IN`.
1060    ///
1061    /// **Measured, and it is the reason the seam exists.** `ClickHouse`
1062    /// 24.10.2 (2026-08-23) answered razeri's emitted re-open with
1063    /// `Code: 47 … UNKNOWN_IDENTIFIER` while resolving the assignment's own
1064    /// subquery — after the statement PARSED, which is what made the
1065    /// distinction visible: the syntax was right and the reach was not.
1066    ///
1067    /// It sits here on [`SqlRenderer`] and **only** here, deliberately not
1068    /// duplicated onto `DialectCapabilities`: `SqlRenderer` is the trait
1069    /// razeri's generators call, so this is the definition that decides, and a
1070    /// second declaration of the same fact is one that can drift.
1071    ///
1072    /// Default `false`: every engine razeri reaches except `ClickHouse` lets a
1073    /// mutation correlate, and the ANSI correlated `UPDATE` those nine compile
1074    /// is unchanged by this flag existing.
1075    fn mutation_reads_only_its_own_rows(&self) -> bool {
1076        false
1077    }
1078
1079    /// **The spelling a SQL statement inside a generated procedure body reads
1080    /// a declared local variable by.**
1081    ///
1082    /// A versioning load binds its clock ONCE — a `Declare` step at the top of
1083    /// the procedure — and every close and every open reads the variable
1084    /// instead of the engine's clock, so there is no daylight between the
1085    /// close of one version and the open of the next. The DECLARATION is the
1086    /// procedure renderer's business; what the load generators need is the
1087    /// reference spelling to embed in the raw statements they compose, and
1088    /// that differs by procedure language:
1089    ///
1090    /// - bare `name` — PL/pgSQL, MySQL/MariaDB, and the default;
1091    /// - `@name` — T-SQL;
1092    /// - `:name` — Snowflake Scripting's bind syntax inside SQL statements;
1093    /// - `$name` — the agnostic format's marker.
1094    ///
1095    /// It mirrors the variable-reference spelling of Razeri's own procedure
1096    /// renderers — the two must agree, and each override says so beside the
1097    /// spelling.
1098    fn render_procedure_variable_reference(&self, name: &str) -> String {
1099        name.to_owned()
1100    }
1101
1102    /// **The statement that binds one query's answer to a declared local.**
1103    ///
1104    /// A procedure body asks a question — how many rows arrived, what the
1105    /// latest timestamp is — and keeps the answer in a variable the rest of the
1106    /// body reads. Every procedural language has that statement and no two
1107    /// spell it alike:
1108    ///
1109    /// - `SELECT COUNT(*) FROM stage INTO v_seen;` — PL/pgSQL;
1110    /// - `SELECT @v_seen = (SELECT COUNT(*) FROM stage);` — T-SQL;
1111    /// - `v_seen := (SELECT COUNT(*) FROM stage);` — Snowflake Scripting;
1112    /// - `EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM stage' INTO v_seen;` —
1113    ///   Oracle's PL/SQL.
1114    ///
1115    /// `query` is the question, already carrying this dialect's variable
1116    /// spelling and with no trailing terminator; `variable` is the bare name of
1117    /// the local, so a grammar that wants the reference spelling asks
1118    /// [`Self::render_procedure_variable_reference`] for it and one that wants
1119    /// the bare name has it already. The answer is a whole statement,
1120    /// terminator included.
1121    ///
1122    /// The default names the query and the target in a form that belongs to no
1123    /// engine, because there is no spelling every engine shares. A dialect
1124    /// rendering its own routine grammar answers this and the body arrives
1125    /// ready to compile.
1126    fn render_procedure_query_into_variable(&self, variable: &str, query: &str) -> String {
1127        format!("EXECUTE {query} INTO {};", self.render_procedure_variable_reference(variable))
1128    }
1129
1130    /// **The statement that stops a procedure body with an error of its own.**
1131    ///
1132    /// A generated load checks what it was given and refuses to go on when the
1133    /// answer is wrong. Every procedural language can stop that way and no two
1134    /// write it alike:
1135    ///
1136    /// - `RAISE EXCEPTION '%', 'the run brought no rows';` — PL/pgSQL;
1137    /// - `THROW 50000, 'the run brought no rows', 1;` — T-SQL;
1138    /// - `SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'the run brought no rows';`
1139    ///   — `MySQL` and `MariaDB`;
1140    /// - `RAISE_APPLICATION_ERROR(-20001, 'the run brought no rows');` —
1141    ///   Oracle's PL/SQL.
1142    ///
1143    /// `message` is the message **already rendered as a SQL expression in this
1144    /// dialect's spelling**: a literal arrives quoted and escaped, and a
1145    /// variable arrives named the way
1146    /// [`Self::render_procedure_variable_reference`] names one, because a body
1147    /// may raise what it read rather than what it was written with. Put it
1148    /// where the statement wants it and do not quote it again. `error_code` is
1149    /// the number the design gave, if it gave one; an engine whose statement
1150    /// takes a number supplies its own when this is `None`. The answer is a
1151    /// whole statement, terminator included.
1152    ///
1153    /// The default names the raise in a form that belongs to no engine. A
1154    /// dialect rendering its own routine grammar answers this and the body
1155    /// arrives ready to compile.
1156    fn render_procedure_raise(&self, message: &str, error_code: Option<i32>) -> String {
1157        match error_code {
1158            Some(code) => format!("RAISE EXCEPTION [{code}] {message};"),
1159            None => format!("RAISE EXCEPTION {message};"),
1160        }
1161    }
1162
1163    /// **The statement that writes a message where an operator can read it.**
1164    ///
1165    /// A load says how it is going, and where that message goes is an engine's
1166    /// own business — a client notice, a print stream, an event table, a
1167    /// result set nobody reads:
1168    ///
1169    /// - `RAISE NOTICE '%', 'loaded';` — PL/pgSQL;
1170    /// - `PRINT 'loaded';` — T-SQL;
1171    /// - `SYSTEM$LOG('INFO', 'loaded');` — Snowflake Scripting;
1172    /// - `DBMS_OUTPUT.PUT_LINE('loaded');` — Oracle's PL/SQL, which has no
1173    ///   levels and ignores the one it is given.
1174    ///
1175    /// `message` arrives the same way [`Self::render_procedure_raise`] takes
1176    /// it: already a SQL expression in this dialect's spelling, quoted where it
1177    /// is a literal. `level` is the severity the design wrote; an engine whose
1178    /// statement carries one reads
1179    /// [`ProcedureLogLevel::as_str`] and spells it the way that statement
1180    /// wants. The answer is a whole statement, terminator included.
1181    ///
1182    /// The default names the level and the message in a form that belongs to no
1183    /// engine. A dialect rendering its own routine grammar answers this and the
1184    /// body arrives ready to compile.
1185    fn render_procedure_log(&self, level: ProcedureLogLevel, message: &str) -> String {
1186        let word = match level {
1187            ProcedureLogLevel::Debug => "DEBUG",
1188            ProcedureLogLevel::Info => "NOTICE",
1189            ProcedureLogLevel::Warning => "WARNING",
1190            ProcedureLogLevel::Error => "EXCEPTION",
1191        };
1192        format!("LOG {word} {message};")
1193    }
1194
1195    /// **The statement a generated load asks a scalar question with and throws
1196    /// the answer away.**
1197    ///
1198    /// A load's guard evaluates one scalar and reads nothing back: it either
1199    /// passes silently or stops the load on the engine's own error. What
1200    /// differs between engines is not the question but the STATEMENT that may
1201    /// carry it, and the difference is a procedure-language rule rather than a
1202    /// SQL one:
1203    ///
1204    /// - a bare `SELECT` is a legal script statement everywhere, and is legal
1205    ///   inside the procedure bodies `MySQL`, `MariaDB` and T-SQL accept, where it
1206    ///   simply hands the caller a result set nobody reads — so that is the
1207    ///   default;
1208    /// - **PL/pgSQL refuses it.** A `SELECT` with no destination inside a
1209    ///   procedure body is `query has no destination for result data`, raised
1210    ///   the moment the procedure is called. PL/pgSQL's own discard form is
1211    ///   `PERFORM`, and the Postgres family overrides this to spell it.
1212    ///
1213    /// The engines that ship no stored procedure inline the same statement into
1214    /// a script, where the default is the only form that runs — and every such
1215    /// engine takes the default, so one answer per dialect is unambiguous.
1216    ///
1217    /// `alias` names the answered column for the forms that answer one; a
1218    /// discard form that answers nothing ignores it.
1219    fn render_discarded_scalar(&self, expression: &str, alias: &str) -> String {
1220        format!("SELECT {expression} AS {alias};")
1221    }
1222
1223    /// **How a script with no stored procedures binds the load's clock once**
1224    /// — same rule as [`Self::render_procedure_variable_reference`], on the
1225    /// engines where a load ships as a flat statement list rather than a
1226    /// procedure body.
1227    ///
1228    /// The statements run FIRST and bind the engine's current moment to
1229    /// `name`; the reference is the expression every close and open in the
1230    /// same run then reads, so a version's `record_end_timestamp` and its
1231    /// successor's `record_start_timestamp` are one value by construction.
1232    /// Every spelling must be RE-RUNNABLE in one session — a second load in
1233    /// the same session rebinds rather than failing — because a load script is
1234    /// run whole and run again.
1235    ///
1236    /// The default is the one shape every engine can carry: a one-row
1237    /// temporary table capturing [`Self::render_current_timestamp`], read back
1238    /// through a scalar subquery. Engines with a native variable idiom
1239    /// override it (`DuckDB`'s `SET VARIABLE`, Databricks' session variables);
1240    /// `ClickHouse` overrides only the temporary-table spelling its own DDL
1241    /// requires.
1242    fn render_inline_clock_binding(&self, name: &str, moment: &str) -> InlineClockBinding {
1243        InlineClockBinding {
1244            statements: vec![
1245                format!("DROP TABLE IF EXISTS {name};"),
1246                format!("CREATE TEMPORARY TABLE {name} AS SELECT {moment} AS load_clock;"),
1247            ],
1248            reference: format!("(SELECT load_clock FROM {name})"),
1249        }
1250    }
1251
1252    /// Whether UPDATE supports TOP(n) clause (`SqlServer`).
1253    fn update_supports_top_clause(&self) -> bool {
1254        false
1255    }
1256
1257    /// Whether UPDATE supports table hints (e.g., `WITH (NOLOCK)`) (`SqlServer`).
1258    fn update_supports_table_hints(&self) -> bool {
1259        false
1260    }
1261
1262    /// Whether UPDATE uses OUTPUT clause positioned before FROM (`SqlServer`).
1263    ///
1264    /// When `true`, RETURNING/OUTPUT is rendered between SET and FROM.
1265    /// When `false`, RETURNING is rendered at the end of the statement.
1266    fn update_uses_output_clause(&self) -> bool {
1267        false
1268    }
1269
1270    /// Whether UPDATE supports a RETURNING clause at the end of the statement.
1271    ///
1272    /// `PostgreSQL`, `CitusDB`: `true`. Others: `false`.
1273    fn update_supports_returning(&self) -> bool {
1274        false
1275    }
1276
1277    // ========================================================================
1278    // INSERT behavior hooks
1279    // ========================================================================
1280
1281    /// Whether INSERT uses OUTPUT clause positioned before VALUES/SELECT (`SqlServer`).
1282    ///
1283    /// When `true`, RETURNING/OUTPUT is rendered between column list and source.
1284    /// When `false`, RETURNING is rendered at the end of the statement.
1285    fn insert_uses_output_clause(&self) -> bool {
1286        false
1287    }
1288
1289    /// Whether INSERT supports RETURNING at the end (`PostgreSQL`, `CitusDB`).
1290    fn insert_supports_returning(&self) -> bool {
1291        false
1292    }
1293
1294    /// Whether INSERT supports table hints (e.g., `WITH (TABLOCK)`) (`SqlServer`).
1295    fn insert_supports_table_hints(&self) -> bool {
1296        false
1297    }
1298
1299    /// Whether INSERT OVERWRITE is supported (Snowflake, Databricks).
1300    fn supports_insert_overwrite(&self) -> bool {
1301        false
1302    }
1303
1304    /// Whether INSERT ... ON CONFLICT is supported (`PostgreSQL`, `CitusDB`).
1305    fn supports_on_conflict(&self) -> bool {
1306        false
1307    }
1308
1309    /// Whether OVERRIDING SYSTEM/USER VALUE clause is supported (`PostgreSQL`).
1310    fn supports_overriding_clause(&self) -> bool {
1311        false
1312    }
1313
1314    /// The keyword for returning data from DML: "OUTPUT" (`SqlServer`) or "RETURNING" (others).
1315    fn returning_clause_keyword(&self) -> &'static str {
1316        "RETURNING"
1317    }
1318
1319    /// Whether OUTPUT ... INTO is supported (`SqlServer`).
1320    fn supports_returning_into(&self) -> bool {
1321        false
1322    }
1323
1324    /// Whether multi-table INSERT (INSERT ALL) is supported (Snowflake).
1325    fn supports_multi_table_insert(&self) -> bool {
1326        false
1327    }
1328
1329    // ========================================================================
1330    // Function/procedure parameter behavior hooks
1331    // ========================================================================
1332
1333    /// Render the security mode clause for function/procedure definitions.
1334    ///
1335    /// Standard SQL: `SECURITY INVOKER`/`SECURITY DEFINER`.
1336    /// T-SQL: `EXECUTE AS CALLER`/`EXECUTE AS OWNER`.
1337    fn render_security_mode(&self, is_definer: bool) -> &'static str {
1338        if is_definer { "SECURITY DEFINER" } else { "SECURITY INVOKER" }
1339    }
1340
1341    /// Render a function parameter name.
1342    ///
1343    /// Standard SQL uses the bare name. T-SQL prefixes with `@`.
1344    fn render_function_parameter_name(&self, name: &str) -> String {
1345        name.to_string()
1346    }
1347
1348    /// Render explicit nullability for a function parameter, if the platform requires it.
1349    ///
1350    /// Most platforms omit nullability in parameter lists. T-SQL native
1351    /// compilation requires explicit `NULL`/`NOT NULL`.
1352    fn render_function_parameter_nullable(&self, _nullable: bool) -> Option<&'static str> {
1353        None
1354    }
1355
1356    /// Render the default-value clause for a function parameter.
1357    ///
1358    /// Standard SQL: `DEFAULT value`. T-SQL: `= value`.
1359    fn render_function_parameter_default(&self, value: &str) -> String {
1360        format!("DEFAULT {value}")
1361    }
1362
1363    /// Render a column name inside a table-valued function definition.
1364    ///
1365    /// Standard SQL uses the bare name. T-SQL uses bracket quoting `[name]`.
1366    fn render_function_column_name(&self, name: &str) -> String {
1367        name.to_string()
1368    }
1369
1370    // ========================================================================
1371    // Partition & generated column rendering hooks
1372    // ========================================================================
1373
1374    /// Render a time partition expression for a column and granularity.
1375    ///
1376    /// Granularity values: `"hourly"`, `"daily"`, `"weekly"`, `"monthly"`,
1377    /// `"quarterly"`, `"yearly"`.
1378    ///
1379    /// Default: `date_trunc('unit', column)` (ANSI / `PostgreSQL` style),
1380    /// mapping granularity names to `date_trunc` units.
1381    fn render_time_partition_expr(&self, column: &str, granularity: &str) -> String {
1382        let unit = match granularity {
1383            "hourly" => "hour",
1384            "daily" => "day",
1385            "weekly" => "week",
1386            "monthly" => "month",
1387            "quarterly" => "quarter",
1388            "yearly" => "year",
1389            other => other,
1390        };
1391        format!("date_trunc('{unit}', {column})")
1392    }
1393
1394    /// Render a DDL partition clause wrapping the expression list.
1395    ///
1396    /// Default: `PARTITION BY (expr)`. Snowflake: `CLUSTER BY (expr)`,
1397    /// Databricks: `PARTITIONED BY (expr)`, `ClickHouse`: `PARTITION BY expr` (no parens).
1398    fn render_partition_clause(&self, expr: &str) -> String {
1399        format!("PARTITION BY ({expr})")
1400    }
1401
1402    /// Render a generated column declaration.
1403    ///
1404    /// Default: `name GENERATED ALWAYS AS (expr) STORED` (`PostgreSQL` style).
1405    fn render_generated_column(&self, name: &str, expr: &str) -> String {
1406        format!("{name} GENERATED ALWAYS AS ({expr}) STORED")
1407    }
1408
1409    // ========================================================================
1410    // Window function capability hooks
1411    // ========================================================================
1412
1413    /// Whether the platform supports the EXCLUDE clause in window frame specifications.
1414    ///
1415    /// `PostgreSQL`, Snowflake, Databricks: `true`. Others: `false`.
1416    fn supports_window_frame_exclude(&self) -> bool {
1417        false
1418    }
1419
1420    /// Whether the platform supports the GROUPS frame mode in window functions.
1421    ///
1422    /// `PostgreSQL`, Databricks: `true`. Others: `false`.
1423    fn supports_window_frame_groups(&self) -> bool {
1424        false
1425    }
1426
1427    /// Whether the platform supports named windows (WINDOW clause).
1428    ///
1429    /// Most platforms: `true`. `ClickHouse`, `DuckDB`, `MariaDB`: `false`.
1430    fn supports_named_windows(&self) -> bool {
1431        true
1432    }
1433
1434    // ========================================================================
1435    // DDL table-scope capability hooks
1436    // ========================================================================
1437
1438    /// Whether the platform uses a `#` prefix for session-scoped temporary table names.
1439    ///
1440    /// SQL Server: `true`. Others: `false`.
1441    fn uses_hash_temp_table_prefix(&self) -> bool {
1442        false
1443    }
1444
1445    /// Whether the platform supports `DECLARE @name TABLE (...)` table variables.
1446    ///
1447    /// SQL Server: `true`. Others: `false`.
1448    fn supports_table_variables(&self) -> bool {
1449        false
1450    }
1451
1452    /// The full CREATE keyword for session-scoped temporary tables.
1453    ///
1454    /// Default: `"CREATE TEMPORARY"`.
1455    /// SQL Server: `"CREATE"` (uses `#name` instead of TEMPORARY keyword).
1456    /// Databricks: `"CREATE OR REPLACE TEMPORARY"`.
1457    fn render_create_temp_table_keyword(&self) -> &'static str {
1458        "CREATE TEMPORARY"
1459    }
1460
1461    /// Whether the platform supports transient tables (e.g. `CREATE TRANSIENT TABLE`).
1462    ///
1463    /// Snowflake: `true`. Others: `false`.
1464    fn supports_transient_tables(&self) -> bool {
1465        false
1466    }
1467
1468    /// Whether the platform supports `ON COMMIT DROP` for transaction-scoped temp tables.
1469    ///
1470    /// `PostgreSQL`, `CitusDB`: `true`. Others: `false`.
1471    fn supports_on_commit_drop(&self) -> bool {
1472        false
1473    }
1474
1475    /// Whether the platform requires an `ENGINE = ...` clause on CREATE TABLE.
1476    ///
1477    /// `ClickHouse`: `true`. Others: `false`.
1478    ///
1479    /// A platform that answers `true` owns its whole engine suffix in ONE place:
1480    /// the `CREATE TABLE` composer emits the engine, then the sorting key, then
1481    /// whatever the resolved storage plan contributes. A storage renderer for
1482    /// such a platform must therefore contribute NEITHER an engine clause nor a
1483    /// sorting key — two engine clauses is `Code: 62 … Syntax error`, and a
1484    /// sorting key that disagrees with the inline `PRIMARY KEY` is
1485    /// `Code: 36 … Primary key must be a prefix of the sorting key` (both
1486    /// measured on `ClickHouse` 24.10.2).
1487    fn requires_table_engine(&self) -> bool {
1488        false
1489    }
1490
1491    /// Whether this platform's table sorting key refuses nullable columns.
1492    ///
1493    /// `ClickHouse`: `true` — a `MergeTree` sorting key containing a
1494    /// `Nullable(…)` column answers `Code: 44 … Sorting key contains nullable
1495    /// columns, but merge tree setting allow_nullable_key is disabled`, measured
1496    /// on 24.10.2 against the control (the same key over a non-nullable column
1497    /// of the same type is accepted). Others: `false`.
1498    ///
1499    /// The materializer asks this before it lets a design's clustering columns
1500    /// into a sorting key, so the answer is a refusal the author can act on
1501    /// rather than a deployment failure they cannot.
1502    fn sorting_key_refuses_nullable_columns(&self) -> bool {
1503        false
1504    }
1505
1506    /// Whether the platform requires a primary key on every table.
1507    ///
1508    /// `ClickHouse` (MergeTree): `true`. Others: `false`.
1509    fn requires_primary_key_in_create_table(&self) -> bool {
1510        false
1511    }
1512
1513    /// Create a boxed [`FormulaRenderer`](crate::FormulaRenderer) for this dialect.
1514    ///
1515    /// Real dialect implementations override this to return `Box::new(Self)`.
1516    /// The default returns an ANSI dialect with standard function names.
1517    fn to_formula_renderer(&self) -> Box<dyn crate::FormulaRenderer> {
1518        Box::new(crate::AnsiDialect)
1519    }
1520}
1521
1522/// Level 2 — Expression rendering with ANSI SQL defaults.
1523pub trait ExpressionRenderer: SqlRenderer {
1524    /// **Render a conversion of `expr` to `target`** — the expression the
1525    /// generated load's typed projection writes for every column.
1526    ///
1527    /// The default is `CAST(expr AS …)` over
1528    /// [`render_cast_type`](SqlRenderer::render_cast_type), the cast vocabulary
1529    /// rather than the column vocabulary: a column may be declared `STRING(36)`
1530    /// where a cast may name only `STRING`, and `TINYINT` where a cast must say
1531    /// `SMALLINT`. Override it where a conversion is not a `CAST` at all —
1532    /// `GoogleSQL` has no cast from `STRING` to `JSON` ("Invalid cast from
1533    /// STRING to JSON") and converts with `PARSE_JSON(expr)` instead.
1534    fn render_cast(&self, expr: &str, target: &DataType) -> String {
1535        format!("CAST({} AS {})", expr, self.render_cast_type(target))
1536    }
1537
1538    /// **Read a value the source spells its own way** into `target`, which is
1539    /// a date or a timestamp.
1540    ///
1541    /// An import arrives with `1/1/2015` in one column and
1542    /// `2015-01-01T00:00:00Z` in the next, and no engine reads either of them
1543    /// the same way as the engine beside it. So the design states the
1544    /// spelling, razeri reads it once into a [`DateFormat`], and this
1545    /// translates that list into whatever this engine's own pattern language
1546    /// is — `MM/DD/YYYY`, `MM/dd/yyyy`, a style number, or a format string
1547    /// this engine calls its own.
1548    ///
1549    /// **A value that is not spelled that way has to come back as NULL.**
1550    /// razeri reads the NULL as *this row did not parse* and writes the row
1551    /// into the table it sets rows aside in, naming the column and the format
1552    /// it was told to expect. An expression that stops the load instead leaves
1553    /// a warehouse half-loaded over one bad row, and one that answers a
1554    /// different day than the value says is worse than either. An engine whose
1555    /// parse reads a value it cannot read — rolling the forty-fifth of a month
1556    /// forward rather than answering nothing — writes its own guard beside the
1557    /// parse; [`DateFormat::shape_pattern`] and
1558    /// [`DateFormat::capture_pattern`] come from the same directive list, so
1559    /// the guard and the parse cannot hold two opinions about one spelling.
1560    ///
1561    /// **A directive this engine cannot say is a refusal, not a guess.**
1562    /// [`DateFormat::translate`] hands back the first directive this engine
1563    /// answered nothing for, and answering `None` here is how that reaches
1564    /// the author — by name, at the door, rather than as a wrong day in the
1565    /// warehouse.
1566    ///
1567    /// `None` — the default — is this engine stating nothing about how it
1568    /// reads a spelled value. razeri then refuses the column and names the
1569    /// engine.
1570    fn render_parse_datetime(
1571        &self,
1572        _expr: &str,
1573        _format: &DateFormat,
1574        _target: &DataType,
1575    ) -> Option<String> {
1576        None
1577    }
1578
1579    /// **Read a value the source writes its own way** into `target`, which is
1580    /// a number.
1581    ///
1582    /// A file writes a price `$2,899.99 `. The symbol, the thousands mark and
1583    /// the space around it are not part of the number, and no two engines
1584    /// agree about what to do with them — one converts the text to nothing,
1585    /// one to `2`, one raises. So the design states the picture, razeri reads
1586    /// it once into a [`NumberFormat`], and this renders the reading in
1587    /// whatever this engine's own words for *take this out* and *convert this*
1588    /// are.
1589    ///
1590    /// **A value that is not written that way has to come back as NULL.**
1591    /// razeri reads the NULL as *this row did not parse* and writes the row
1592    /// into the table it sets rows aside in, naming the column and the picture
1593    /// it was told to expect. An expression that stops the load instead leaves
1594    /// a warehouse half-loaded over one bad row, and one that answers `0` for
1595    /// text that was never a number is worse than either. An engine whose
1596    /// conversion raises, or answers a number for text that is not one, writes
1597    /// a guard in front of it; [`NumberFormat::shape_pattern`] is built from
1598    /// the same picture the marks come from, so the guard and the reading
1599    /// cannot hold two opinions about one spelling.
1600    ///
1601    /// `None` — the default — is this engine stating nothing about how it
1602    /// reads a written amount. razeri then refuses the column and names the
1603    /// engine. `None` is also the right answer for a target that is not a
1604    /// number: a picture says how an amount is written, and there is nothing
1605    /// else for it to be about.
1606    ///
1607    /// [`NumberFormat`]: crate::number_format::NumberFormat
1608    /// [`NumberFormat::shape_pattern`]: crate::number_format::NumberFormat::shape_pattern
1609    fn render_parse_number(
1610        &self,
1611        _expr: &str,
1612        _format: &NumberFormat,
1613        _target: &DataType,
1614    ) -> Option<String> {
1615        None
1616    }
1617
1618    /// Render string concatenation.
1619    fn render_concat(&self, parts: &[String]) -> String {
1620        format!("CONCAT({})", parts.join(", "))
1621    }
1622
1623    /// Render a COALESCE expression.
1624    fn render_coalesce(&self, parts: &[String]) -> String {
1625        format!("COALESCE({})", parts.join(", "))
1626    }
1627
1628    /// Render a string aggregation function.
1629    fn render_string_agg(&self, expr: &str, separator: &str) -> String {
1630        format!("STRING_AGG({expr}, '{separator}')")
1631    }
1632}
1633
1634/// A foreign key's referential action — SQL's own five words, no synonyms.
1635///
1636/// Lives here (rather than in the table model) so a dialect can answer
1637/// [`DdlRenderer::supports_foreign_key_action`] about a specific action
1638/// without depending on the modeling crate.
1639#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1640pub enum ReferentialAction {
1641    /// Also delete or update the referencing rows
1642    Cascade,
1643    /// Set the referencing columns to NULL
1644    SetNull,
1645    /// Reject the delete or update; some engines defer the check
1646    Restrict,
1647    /// Reject the delete or update (the SQL default)
1648    NoAction,
1649    /// Set the referencing columns to their default values
1650    SetDefault,
1651}
1652
1653impl ReferentialAction {
1654    /// Parses the SQL words, case-insensitively; underscores read as spaces
1655    /// so `set_null` and `SET NULL` both land.
1656    ///
1657    /// # Errors
1658    /// Returns a plain message listing the valid words when the input is not
1659    /// one of them.
1660    pub fn from_sql_word(word: &str) -> Result<Self, String> {
1661        match word.trim().replace('_', " ").to_uppercase().as_str() {
1662            "CASCADE" => Ok(Self::Cascade),
1663            "SET NULL" => Ok(Self::SetNull),
1664            "RESTRICT" => Ok(Self::Restrict),
1665            "NO ACTION" => Ok(Self::NoAction),
1666            "SET DEFAULT" => Ok(Self::SetDefault),
1667            _ => Err(format!(
1668                "'{word}' is not a referential action. Use one of: CASCADE, SET NULL, \
1669                 RESTRICT, NO ACTION, SET DEFAULT."
1670            )),
1671        }
1672    }
1673}
1674
1675impl std::fmt::Display for ReferentialAction {
1676    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1677        match self {
1678            Self::Cascade => write!(f, "CASCADE"),
1679            Self::SetNull => write!(f, "SET NULL"),
1680            Self::Restrict => write!(f, "RESTRICT"),
1681            Self::NoAction => write!(f, "NO ACTION"),
1682            Self::SetDefault => write!(f, "SET DEFAULT"),
1683        }
1684    }
1685}
1686
1687/// Level 3 — DDL rendering with reasonable defaults.
1688pub trait DdlRenderer: SqlRenderer {
1689    /// Prefix for CREATE TABLE statements.
1690    fn render_create_table_prefix(&self) -> &'static str {
1691        "CREATE TABLE"
1692    }
1693
1694    /// Whether this engine carries the given foreign-key referential action
1695    /// (`ON DELETE …` / `ON UPDATE …`) in `CREATE TABLE` DDL.
1696    ///
1697    /// The ANSI default says yes to all five words. A dialect answers `false`
1698    /// for an action its engine rejects or silently never enforces; the
1699    /// renderer then keeps the author's declaration visible as a plain
1700    /// comment instead of emitting a clause the engine would refuse — a
1701    /// declared action never vanishes without a trace.
1702    fn supports_foreign_key_action(&self, action: ReferentialAction) -> bool {
1703        let _ = action;
1704        true
1705    }
1706
1707    /// Prefix for CREATE VIEW statements.
1708    fn render_create_view_prefix(&self) -> &'static str {
1709        "CREATE OR REPLACE VIEW"
1710    }
1711
1712    /// Render a column definition (name, type, nullability).
1713    fn render_column_definition(&self, name: &str, dt: &DataType, nullable: bool) -> String {
1714        let quoted = self.quote_identifier(name);
1715        let (type_str, null_str) = if nullable {
1716            (self.render_nullable_data_type(dt), "")
1717        } else {
1718            (self.render_data_type(dt), " NOT NULL")
1719        };
1720        format!("{quoted} {type_str}{null_str}")
1721    }
1722
1723    /// **The statement this engine takes for one change to a table that
1724    /// already holds rows**, or `None` where it has none.
1725    ///
1726    /// Razeri classifies every difference between two deploys and hands the
1727    /// ones that lose nothing to this method, one at a time. What comes back
1728    /// goes into the file an operator runs; `None` means the change is named in
1729    /// the plan with this engine's name beside it, so the reader learns which
1730    /// engine has no statement rather than that no engine does.
1731    ///
1732    /// # The default, and why it is the only one
1733    ///
1734    /// `ALTER TABLE … ADD COLUMN … <type>` is the one spelling every engine
1735    /// Razeri targets accepts, so a dialect that answers nothing still writes
1736    /// the commonest evolution there is: a column that was not there yesterday.
1737    /// The column is declared without `NOT NULL`, which is the only form that
1738    /// can be added to a table that already holds rows.
1739    ///
1740    /// Every other change answers `None` by default. A widening and a record
1741    /// that gained a field are each spelled three or four ways across the
1742    /// engines Razeri reaches, and a default picked from one of them writes SQL
1743    /// the others reject — a statement handed to an operator who then finds out
1744    /// their engine will not take it. A dialect that has measured its engine
1745    /// says so here, and the ones that have not say nothing.
1746    ///
1747    /// The statement comes back without a terminator; the caller separates
1748    /// statements the way this engine separates them.
1749    fn render_column_evolution(&self, change: &ColumnEvolution<'_>) -> Option<String> {
1750        match change {
1751            ColumnEvolution::AddColumn { schema, table, column, data_type } => {
1752                let target = self.qualify_name(*schema, table);
1753                let name = self.quote_identifier(column);
1754                let rendered = self.render_nullable_data_type(data_type);
1755                Some(format!("ALTER TABLE {target} ADD COLUMN {name} {rendered}"))
1756            }
1757            ColumnEvolution::WidenColumn { .. } | ColumnEvolution::AddNestedField { .. } => None,
1758        }
1759    }
1760
1761    /// Render the surrogate identity column — a plain, ungenerated `BIGINT NOT
1762    /// NULL` on **every** dialect.
1763    ///
1764    /// **razeri's surrogate keys are logical and DETERMINATE, never
1765    /// engine-generated.** The identity value is a hash
1766    /// derived from the entity's identity grain, WRITTEN BY THE LOAD
1767    /// (`materialize_core`, `sid_expression`), so the same design loaded with the
1768    /// same data converges to the same key on `Postgres`, `Snowflake`, `MariaDB`,
1769    /// `ClickHouse` — every target. A database-assigned `AUTO_INCREMENT` /
1770    /// `IDENTITY(1,1)` / `GENERATED ALWAYS AS IDENTITY` would be sequential,
1771    /// load-order-dependent and **non-portable** — a different key per database
1772    /// for the same row — which is the one thing a portable key may not be. So no
1773    /// dialect emits a generator here; every one renders a plain column and the
1774    /// load fills it.
1775    ///
1776    /// `SQLite` narrows the type to `INTEGER` and `ClickHouse`/`DuckDB` carry
1777    /// their own overrides only to document the same plain shape; no override
1778    /// re-introduces a generator.
1779    ///
1780    /// # The type is the engine's own word for it
1781    ///
1782    /// This used to write the word `BIGINT` itself, on the reasoning that every
1783    /// engine has one. Not every engine does: Oracle has no `BIGINT` and
1784    /// refused the column outright — `ORA-00902: invalid datatype` — so the
1785    /// table holding it could not be created and nothing downstream of it could
1786    /// load. The shape is the decision here, a plain ungenerated column; how
1787    /// this engine spells a 64-bit integer is the dialect's answer and is asked
1788    /// for rather than assumed. An engine whose word for it *is* `BIGINT` gets
1789    /// the same column it always did.
1790    fn render_identity_column(&self, name: &str, data_type: &DataType) -> String {
1791        let quoted = self.quote_identifier(name);
1792        let rendered = self.render_data_type(data_type);
1793        format!("{quoted} {rendered} NOT NULL")
1794    }
1795
1796    /// Whether this platform's engine actually **assigns** an identity column's
1797    /// value, rather than merely tolerating the declaration.
1798    ///
1799    /// **This is a dialect fact, not a field fact, and the difference is a
1800    /// deploy-time failure.** `Field::identity` says razeri intends the engine
1801    /// to generate the value; whether it does depends entirely on the target.
1802    /// `render_identity_column`'s own table above records that `DuckDB`,
1803    /// `SQLite` and `ClickHouse` have **no native identity** — there the column
1804    /// comes out as a plain `NOT NULL` and **the writer must supply it.**
1805    ///
1806    /// The default is `false` because the default `render_identity_column`
1807    /// emits no generation clause: a dialect that overrides the rendering to
1808    /// generate values must override this too, and
1809    /// `the_identity_capability_matches_what_each_dialect_renders` holds the two
1810    /// in step so they cannot drift.
1811    ///
1812    /// Read by the deploy bundle's write contract: a column
1813    /// the engine does not generate is a column a hand-written `INSERT` owes.
1814    fn generates_identity_values(&self) -> bool {
1815        false
1816    }
1817
1818    /// **The latest instant this platform can hold EXACTLY in the column it
1819    /// renders for `data_type`** — the ceiling the refuse-by-name rule compares
1820    /// against, or `None` for *no declared ceiling*.
1821    ///
1822    /// Spelled `YYYY-MM-DD HH:MM:SS`, which is how razeri writes a timestamp
1823    /// literal (`razeri_materialize::convert::OPEN_INTERVAL_SENTINEL`), so the
1824    /// comparison is a string comparison over a format that sorts
1825    /// chronologically. A ceiling and a value in the same spelling cannot be
1826    /// compared wrongly by a parser neither of them goes through.
1827    ///
1828    /// # What the refusal contract asks of this
1829    ///
1830    /// > A value razeri cannot store exactly on a target platform is a
1831    /// > deploy-time refusal naming the value, the column, the platform and the
1832    /// > ceiling — never a truncation, never a saturated write.
1833    ///
1834    /// The *refusal* lives in `razeri_materialize::bundle`, which is the one
1835    /// place it is built — the function is `refuse_unrepresentable_values`, and
1836    /// it is reached from `generate_bundle` before a single file is rendered.
1837    /// This is only where a platform declares the number.
1838    ///
1839    /// # A stated variance from the decision's original wording
1840    ///
1841    /// The refusal was decided as a *deploy-time* act, homed in the
1842    /// prerequisites step. Razeri refuses **earlier**: at bundle
1843    /// generation, by declining to emit the bundle at all. That is strictly
1844    /// ahead of deploy time and cannot be skipped by an operator who never runs
1845    /// the script — a deliberate strengthening, recorded rather
1846    /// than glossed. The consequence worth knowing: a *hand-assembled* bundle,
1847    /// or one from an older razeri, is not re-checked at deploy time by
1848    /// anything. **The default is `None`, which
1849    /// means "razeri has no measured ceiling for this platform and type" and not
1850    /// "this platform holds everything"** — an unmeasured platform gets no
1851    /// refusal rather than an invented one.
1852    ///
1853    /// # The ceiling is a property of the TYPE, not of the platform
1854    ///
1855    /// That is the half a platform-flat capability would get wrong, and it is
1856    /// measured rather than assumed (reading the renderers):
1857    ///
1858    /// | platform | `Timestamp(p)` renders | ceiling | `TimestampTz(p)` renders | ceiling |
1859    /// |---|---|---|---|---|
1860    /// | `MySQL`, `MariaDB` | `DATETIME(p)` | `None` — the end of the four-digit calendar is not a constraint | `TIMESTAMP(p)` | **`2038-01-19 03:14:07`** |
1861    /// | `ClickHouse` | `DateTime64(p)` | `2299-12-31 23:59:59` at p ≤ 6, **`2262-04-11 23:47:16`** at p ≥ 7 | `DateTime64(p,'UTC')` | the same |
1862    /// | everything else | — | not measured → `None` | — | `None` |
1863    ///
1864    /// On `MySQL` the two razeri type variants land on *different engine types*
1865    /// and get different answers: `Timestamp` renders as the
1866    /// non-converting `DATETIME` — the type whose semantics match the declared
1867    /// non-tz timestamp — which runs to the end of the calendar and so
1868    /// declares no ceiling at all, while `TimestampTz` keeps `TIMESTAMP` and its
1869    /// 32-bit epoch boundary. A check keyed on the platform alone would either
1870    /// miss the real ceiling or refuse a column that is fine. On `ClickHouse`
1871    /// the ceiling moves with the **declared precision** of one type:
1872    /// `DateTime64(9)` is int64 nanoseconds from the epoch and stops at
1873    /// 2262-04-11, while `DateTime64(6)` reaches 2299.
1874    ///
1875    /// Parameters:
1876    /// - `data_type`: the column's declared type, as the design carries it —
1877    ///   not the rendered string, because the precision is what moves the answer
1878    ///   and re-parsing it back out of `DateTime64(9)` would be a second reading
1879    ///   of what the dialect already knows.
1880    fn representable_timestamp_ceiling(&self, data_type: &DataType) -> Option<&'static str> {
1881        let _ = data_type;
1882        None
1883    }
1884
1885    /// Whether the platform requires every auto-increment / identity column to
1886    /// be the leading column of an index (PK or KEY).
1887    ///
1888    /// `MySQL` / `MariaDB`: `true` (otherwise error 1075). Others: `false`.
1889    ///
1890    /// When this is `true` and an identity column is not already the leading
1891    /// column of the primary key, the DDL renderer must emit an additional
1892    /// `KEY (identity_col)` clause on the table.
1893    fn auto_increment_requires_key(&self) -> bool {
1894        false
1895    }
1896
1897    /// Prefix for DROP TABLE statements.
1898    fn render_drop_table_prefix(&self, if_exists: bool) -> String {
1899        if if_exists { "DROP TABLE IF EXISTS".to_string() } else { "DROP TABLE".to_string() }
1900    }
1901
1902    /// Prefix for DROP VIEW statements.
1903    fn render_drop_view_prefix(&self, if_exists: bool) -> String {
1904        if if_exists { "DROP VIEW IF EXISTS".to_string() } else { "DROP VIEW".to_string() }
1905    }
1906
1907    /// Prefix for CREATE SCHEMA / CREATE NAMESPACE.
1908    fn render_create_schema_prefix(&self, if_not_exists: bool) -> String {
1909        if if_not_exists {
1910            "CREATE SCHEMA IF NOT EXISTS".to_string()
1911        } else {
1912            "CREATE SCHEMA".to_string()
1913        }
1914    }
1915
1916    /// Render a complete CREATE SCHEMA statement.
1917    ///
1918    /// Dialects that need fundamentally different syntax (e.g. T-SQL wrapping
1919    /// in `IF SCHEMA_ID(...) IS NULL EXEC(...)`) should override this method.
1920    /// The default concatenates [`Self::render_create_schema_prefix`] with the name.
1921    fn render_create_schema_statement(&self, name: &str, if_not_exists: bool) -> String {
1922        let prefix = self.render_create_schema_prefix(if_not_exists);
1923        format!("{prefix} {name}")
1924    }
1925
1926    /// Render a table comment (platform-specific syntax).
1927    ///
1928    /// Like [`Self::render_column_comment`], nothing in the tree calls this and
1929    /// no dialect overrides it — **and unlike its sibling it has not been
1930    /// measured.** Read that method's table before wiring either: the ANSI
1931    /// spelling is a hard syntax error on five of the ten platforms razeri
1932    /// renders for at the column level, and this form was not probed.
1933    fn render_table_comment(&self, table: &str, comment: &str) -> Option<String> {
1934        Some(format!("COMMENT ON TABLE {table} IS '{comment}'"))
1935    }
1936
1937    /// Render a column comment — **not wired to anything, and measured**.
1938    ///
1939    /// # Nothing calls this
1940    ///
1941    /// No caller in the tree, no dialect override, and, until measured, no
1942    /// execution against a live engine — the exact state the column `DEFAULT`
1943    /// clause was in when it turned out to be refused outright on four platforms
1944    /// of ten and silently wrong on a fifth. Moving the open
1945    /// interval's meaning here was proposed **on the condition that it be measured first**,
1946    /// because *"a comment that fails to compile is a worse outcome than the
1947    /// clause it replaced"*.
1948    ///
1949    /// # It was measured, on ten platforms, and this ANSI form is right on five
1950    ///
1951    /// Executed against six containers, in-process `DuckDB` and
1952    /// `SQLite`, and both live cloud accounts:
1953    ///
1954    /// | platform | this `COMMENT ON COLUMN` | inline `COMMENT` in `CREATE TABLE` | what it does take |
1955    /// |---|---|---|---|
1956    /// | `PostgreSQL`, Citus | ✅ | ❌ syntax error | this, as a separate statement |
1957    /// | `DuckDB` | ✅ | ❌ syntax error | this, as a separate statement |
1958    /// | Snowflake, Databricks | ✅ | ✅ | either |
1959    /// | `ClickHouse` | ❌ `SYNTAX_ERROR` (code 62) | ✅ | inline, or `ALTER TABLE … COMMENT COLUMN c '…'` |
1960    /// | `MySQL`, `MariaDB` | ❌ `ERROR 1064` | ✅ | inline, or `ALTER TABLE … MODIFY <whole column definition> COMMENT '…'` |
1961    /// | SQL Server | ❌ `Msg 156, Incorrect syntax near the keyword 'ON'` | ❌ `Msg 102` | `EXEC sp_addextendedproperty …` — a stored procedure, not DDL |
1962    /// | `SQLite` | ❌ syntax error | ❌ syntax error | **nothing at all** |
1963    ///
1964    /// Four mechanisms for ten platforms, and two of them carry a hazard the
1965    /// `DEFAULT` clause never had:
1966    ///
1967    /// - `MySQL`'s and `MariaDB`'s only `ALTER` route **redefines the column**.
1968    ///   Measured: a `MODIFY` that did not restate `NOT NULL` left
1969    ///   `is_nullable = YES` behind it. A documentation mechanism that can
1970    ///   silently de-constrain a column is strictly worse than the clause it
1971    ///   replaces.
1972    /// - SQL Server's `sp_addextendedproperty` **is not idempotent**:
1973    ///   `Msg 15233 — Property 'MS_Description' already exists`. Razeri emits
1974    ///   `CREATE TABLE IF NOT EXISTS` so a deploy bundle can be re-applied; a
1975    ///   comment mechanism that fails on the second run takes that away.
1976    ///
1977    /// # So the escape hatch was taken
1978    ///
1979    /// Q1's answer included its own fall-back — *"if the measurement comes back
1980    /// ugly, take the simpler answer: no comment, and the bundle README carries
1981    /// the contract"* — and the measurement is ugly. **Razeri emits no column
1982    /// comment.** The open interval's meaning ships instead in the deploy
1983    /// bundle's generated *write contract*
1984    /// (`razeri_materialize::bundle::write_contract`, Q5), which is prose an
1985    /// operator reads, is right on all ten platforms because no engine parses
1986    /// it, and is owed regardless of how this measurement came out.
1987    ///
1988    /// The hook stays, unchanged and unwired, for the same reason
1989    /// `render_column_default` stays: a dialect that genuinely wants to emit one
1990    /// has somewhere to say so. **A dialect that overrides this owes a
1991    /// measurement first** — the ANSI spelling below is a hard syntax error on
1992    /// five of the ten platforms razeri renders for.
1993    fn render_column_comment(&self, table: &str, column: &str, comment: &str) -> Option<String> {
1994        Some(format!("COMMENT ON COLUMN {table}.{column} IS '{comment}'"))
1995    }
1996
1997    /// Whether to render IF NOT EXISTS on CREATE TABLE.
1998    fn supports_if_not_exists(&self) -> bool {
1999        true
2000    }
2001
2002    /// Render a column's `DEFAULT` clause — **keyword included** — or decline it.
2003    ///
2004    /// `None` means *this dialect renders no default clause for this value*,
2005    /// and Razeri's column renderer omits the whole clause. The ANSI default
2006    /// returns `None` for every value, which
2007    /// is the write-contract rule expressed where the renderer can see it:
2008    /// **razeri emits no
2009    /// column `DEFAULT` clause in rendered DDL, on any platform, for any value —
2010    /// a value the generated load owns is written by the load, never by the
2011    /// schema.**
2012    ///
2013    /// *Read that qualifier deliberately.* The sentence is about the DDL an engine
2014    /// executes. The `.raz.sql` **agnostic interchange** form —
2015    /// `Table::to_ddl()`, which `Table::from_ddl()` parses back — still
2016    /// serializes `Field::default_value` as `DEFAULT <value>`, because that is
2017    /// where a design's declared default survives a save and a load.
2018    ///
2019    /// **That exception is a serializer, not a platform, and the word "agnostic"
2020    /// names both.** `to_ddl()` renders through a free function and consults no
2021    /// dialect; you reach it by calling it, never by choosing a target.
2022    /// `SchemaObject::render_ddl` short-circuits to it only for a renderer whose
2023    /// [`SqlRenderer::is_agnostic_dialect`] answers `true` — which defaults to
2024    /// `false` here and is overridden by exactly one type in the tree,
2025    /// `AnsiDialect`, which no registration hands out. The platform the registry
2026    /// publishes under the name `agnostic` is `AgnosticDialect`; it does **not**
2027    /// override the method, so it declines the clause like every other target.
2028    /// The rule therefore holds on all eleven registered platforms, `agnostic`
2029    /// included, on every rendering path, and a regression test pins it.
2030    ///
2031    /// # Why the keyword lives in here
2032    ///
2033    /// It used to be concatenated by the caller — `parts.push(format!("DEFAULT
2034    /// {rendered}"))` — so the hook could rewrite a value and **could not decline
2035    /// a clause**: a dialect returning `""` emitted a bare dangling `DEFAULT`.
2036    /// This is the same correction made for
2037    /// statement boundaries: *the emission declares the boundary; a consumer
2038    /// never infers it.*
2039    ///
2040    /// # Why the hook survives the decision
2041    ///
2042    /// `Field::default_value` stays on the struct, so the design
2043    /// still knows what a column's intended default is, and a future dialect that
2044    /// genuinely wants to emit one has somewhere to say so. The distinction being
2045    /// bought is between **what razeri knows** and **what razeri emits**.
2046    ///
2047    /// A dialect that overrides this owes the whole clause, keyword and all —
2048    /// `Some(format!("DEFAULT {value}"))` — because there is nowhere else it is
2049    /// added.
2050    fn render_column_default(&self, value: &str) -> Option<String> {
2051        let _ = value;
2052        None
2053    }
2054
2055    /// **Spell an author's OWN declared default in this engine's words.**
2056    ///
2057    /// This is the other sense of "default", and the seam a dialect had no way
2058    /// to reach. [`render_column_default`](Self::render_column_default) governs
2059    /// the tables Razeri's generated load owns, and it declines everywhere by
2060    /// design. A `tile: base` table is the ruled exception: the author owns
2061    /// every column, no generated statement writes into it, and the declared
2062    /// default is an engine constraint they asked for — so it is rendered, on
2063    /// every platform, and it used to be rendered **verbatim**.
2064    ///
2065    /// Verbatim is a portability hole with the design's own words in it. A
2066    /// design declaring `default: CURRENT_TIMESTAMP` is asking for *the current
2067    /// moment*, not for those eighteen characters; on `BigQuery` those
2068    /// characters name the zone-aware type, the column Razeri builds is
2069    /// `DATETIME`, and the `CREATE TABLE` is refused outright — *"Column
2070    /// default expression has type TIMESTAMP which cannot be assigned to column
2071    /// type DATETIME"*. The design was right and had nowhere to be translated.
2072    ///
2073    /// The default returns the value unchanged, which is exactly today's
2074    /// behaviour on every platform, so a dialect that says nothing renders what
2075    /// it always did.
2076    ///
2077    /// # A dialect overriding this owes a measurement, and owes restraint
2078    ///
2079    /// Translate an expression whose *meaning* your engine spells differently.
2080    /// Do not rewrite an author's literal, do not widen a type, and do not
2081    /// silently drop a default you cannot express — an author who declared one
2082    /// and got none has a column that fills with NULL and a document that says
2083    /// otherwise. Where the engine has no equivalent at all, leave the value
2084    /// alone and let the engine refuse it: a loud failure belongs to the value
2085    /// the author wrote, and a quiet substitution belongs to nobody.
2086    ///
2087    /// Parameters:
2088    /// - `value`: the default exactly as the design declares it.
2089    /// - `column_type`: the type of the column it is being assigned to —
2090    ///   load-bearing, because the same expression is legal against one type
2091    ///   and refused against another on the same engine.
2092    fn render_declared_column_default(&self, value: &str, column_type: &DataType) -> String {
2093        let _ = column_type;
2094        value.to_owned()
2095    }
2096
2097    // ═══════════════════════════════════════════════════════════════
2098    // DDL Extensions
2099    // ═══════════════════════════════════════════════════════════════
2100
2101    /// Render a BEGIN transaction statement.
2102    ///
2103    /// `PostgreSQL`: `BEGIN`, `SqlServer`: `BEGIN TRANSACTION`, `ClickHouse`: empty (no DDL transactions)
2104    fn render_begin_transaction(&self) -> &'static str {
2105        "BEGIN"
2106    }
2107
2108    /// Render a COMMIT transaction statement.
2109    fn render_commit(&self) -> &'static str {
2110        "COMMIT"
2111    }
2112
2113    /// Whether the platform supports DDL transactions.
2114    fn supports_ddl_transactions(&self) -> bool {
2115        true
2116    }
2117
2118    /// Render a DROP FUNCTION statement with optional parameter types for overloaded functions.
2119    fn render_drop_function(&self, name: &str, param_types: &[&str], if_exists: bool) -> String {
2120        let exists = if if_exists { " IF EXISTS" } else { "" };
2121        if param_types.is_empty() {
2122            format!("DROP FUNCTION{exists} {name}")
2123        } else {
2124            format!("DROP FUNCTION{exists} {name}({})", param_types.join(", "))
2125        }
2126    }
2127
2128    /// Render the drop keyword for a SQL object type.
2129    fn render_drop_object_keyword(&self, obj_type: &str) -> String {
2130        obj_type.to_uppercase()
2131    }
2132
2133    /// Render the CREATE VIEW keyword/prefix with `secure/global_temp` modifiers.
2134    fn render_create_view_keyword(&self, secure: bool, global_temp: bool) -> String {
2135        let mut parts = vec!["CREATE OR REPLACE"];
2136        if secure {
2137            parts.push("SECURE");
2138        }
2139        if global_temp {
2140            parts.push("GLOBAL TEMPORARY");
2141        }
2142        parts.push("VIEW");
2143        parts.join(" ")
2144    }
2145
2146    /// Render an IF EXISTS clause for ALTER TABLE DROP COLUMN/CONSTRAINT.
2147    ///
2148    /// Some platforms (`SqlServer`, Databricks, `MariaDB`) don't support IF EXISTS in ALTER DROP.
2149    fn supports_alter_drop_if_exists(&self) -> bool {
2150        true
2151    }
2152
2153    // ═══════════════════════════════════════════════════════════════
2154    // Drop Behavior Hooks
2155    // ═══════════════════════════════════════════════════════════════
2156
2157    /// Whether DROP INDEX uses ON syntax: `DROP INDEX idx ON table` (`SqlServer`).
2158    fn drop_index_uses_on_syntax(&self) -> bool {
2159        false
2160    }
2161
2162    /// **Whether this engine checks the constraints it accepts.**
2163    ///
2164    /// The default is [`ConstraintEnforcement::Enforced`] — eight of Razeri's
2165    /// registered platforms, and the safe assumption for a dialect that has not
2166    /// said, because *"this engine guarantees nothing"* is the unusual claim.
2167    ///
2168    /// Answer [`ConstraintEnforcement::DeclaredOnly`] where a `PRIMARY KEY`,
2169    /// `FOREIGN KEY` or `UNIQUE` is accepted and never checked. Two engines are
2170    /// that, in two different ways, and the type carries both:
2171    ///
2172    /// - `BigQuery` has the words. `NOT ENFORCED` is the only legal spelling,
2173    ///   so the keyword goes on the constraint line and the engine makes the
2174    ///   declaration honest by itself.
2175    /// - `Redshift` has no words. It takes all three constraints, checks none,
2176    ///   and renders DDL a reader cannot tell from `PostgreSQL`'s — so the note
2177    ///   is written above the constraint list instead, and it is the only place
2178    ///   that DDL says what it means.
2179    ///
2180    /// [`key_constraint_suffix`](Self::key_constraint_suffix) reads this, so
2181    /// stating enforcement states the suffix too.
2182    fn constraint_enforcement(&self) -> ConstraintEnforcement {
2183        ConstraintEnforcement::Enforced
2184    }
2185
2186    /// **The words a key constraint must carry to be accepted at all.**
2187    ///
2188    /// Appended to a table-level `PRIMARY KEY (…)` and to a
2189    /// `FOREIGN KEY (…) REFERENCES …`. Empty on every engine that enforces its
2190    /// keys, and empty on a declaring engine that has no keyword for it.
2191    ///
2192    /// The default reads
2193    /// [`constraint_enforcement`](Self::constraint_enforcement), so the two are
2194    /// one fact rather than two that must be kept in step. Override this only
2195    /// for a keyword a key constraint needs for some *other* reason than
2196    /// enforcement — there is no such engine in Razeri's set today.
2197    fn key_constraint_suffix(&self) -> &'static str {
2198        self.constraint_enforcement().keyword()
2199    }
2200
2201    /// Whether a table-level `UNIQUE (…)` constraint is part of the grammar.
2202    ///
2203    /// `false` on an engine that has no such constraint at all, where the
2204    /// declaration is left out of the `CREATE TABLE` and named in a comment
2205    /// beside it rather than emitted and refused.
2206    fn supports_unique_constraint(&self) -> bool {
2207        true
2208    }
2209
2210    /// Whether a table-level `CHECK (…)` constraint is part of the grammar.
2211    ///
2212    /// `false` on an engine that has none, with the same treatment as
2213    /// [`supports_unique_constraint`](Self::supports_unique_constraint): named
2214    /// in a comment, never written as SQL the engine will reject.
2215    fn supports_check_constraint(&self) -> bool {
2216        true
2217    }
2218
2219    /// Whether the platform supports CASCADE/RESTRICT in DROP statements.
2220    ///
2221    /// True for `PostgreSQL`, `CitusDB`, `DuckDB`, Snowflake, Agnostic.
2222    /// False for `SqlServer`, `ClickHouse`, Databricks, `MySQL`, `MariaDB`.
2223    fn supports_cascade_drop(&self) -> bool {
2224        true
2225    }
2226
2227    /// Whether `COMMENT ON PROCEDURE` is part of the grammar.
2228    ///
2229    /// `true` almost everywhere, and **false on `Redshift`**, where
2230    /// `COMMENT ON FUNCTION`, `COMMENT ON VIEW` and `COMMENT ON COLUMN` all
2231    /// exist and the procedure form alone does not: *"syntax error at or near
2232    /// `PROCEDURE`"*, measured on a live workgroup. A description is worth
2233    /// having and is not worth a statement that stops the script, so a
2234    /// declining engine simply carries no description on its procedures.
2235    fn supports_procedure_comment(&self) -> bool {
2236        true
2237    }
2238
2239    /// Whether the platform supports function argument types in DROP FUNCTION.
2240    ///
2241    /// False for `ClickHouse` (no overloaded functions).
2242    fn supports_drop_function_arg_types(&self) -> bool {
2243        true
2244    }
2245
2246    /// Whether the platform supports SYNC option for DROP (`ClickHouse`).
2247    fn supports_sync_drop(&self) -> bool {
2248        false
2249    }
2250
2251    /// Whether the platform supports FORCE option for DROP (Databricks).
2252    fn supports_force_drop(&self) -> bool {
2253        false
2254    }
2255
2256    /// Whether the platform supports CASCADE in ALTER TABLE DROP.
2257    ///
2258    /// Only `PostgreSQL`, `CitusDB`, `DuckDB` support CASCADE in ALTER TABLE DROP.
2259    fn supports_alter_drop_cascade(&self) -> bool {
2260        false
2261    }
2262}
2263
2264/// **One row, written once** — the pieces of an INSERT that must do nothing
2265/// when a row already carries the same key.
2266///
2267/// The key value is not a field of its own: it is whichever `values` entry sits
2268/// at the same index as the key column in `columns`. Two fields that must agree
2269/// are two fields free to disagree, and the one they would disagree about here
2270/// is the one that decides whether a second write is a duplicate.
2271#[derive(Debug, Clone, Copy)]
2272pub struct InsertIfAbsent<'a> {
2273    /// The target table, already spelled the way the caller wants it written —
2274    /// schema-qualified or bare.
2275    pub table: &'a str,
2276    /// The columns being written, in the order `values` supplies them.
2277    pub columns: &'a [&'a str],
2278    /// One value expression per column.
2279    pub values: &'a [String],
2280    /// The columns whose equality means *this row is already here*.
2281    pub key_columns: &'a [&'a str],
2282}
2283
2284impl InsertIfAbsent<'_> {
2285    /// The value expression written into `column`, if it is being written.
2286    #[must_use]
2287    pub fn value_of(&self, column: &str) -> Option<&str> {
2288        let at = self.columns.iter().position(|name| *name == column)?;
2289        self.values.get(at).map(String::as_str)
2290    }
2291
2292    /// `key = value AND …` — the test for *already here*, in terms of the
2293    /// values this INSERT would have written.
2294    ///
2295    /// `qualifier` prefixes the column side, which the guarded form needs so
2296    /// the probe's own row is named unambiguously.
2297    ///
2298    /// # Panics
2299    ///
2300    /// When a key column is not among the columns being written. A key the
2301    /// INSERT does not supply cannot be tested for, and silently dropping it
2302    /// from the predicate would make the guard match everything.
2303    #[must_use]
2304    pub fn key_predicate(&self, qualifier: &str) -> String {
2305        self.key_columns
2306            .iter()
2307            .map(|column| {
2308                let value = self
2309                    .value_of(column)
2310                    .unwrap_or_else(|| panic!("{column} is a key column and must be written"));
2311                format!("{qualifier}{column} = {value}")
2312            })
2313            .collect::<Vec<_>>()
2314            .join(" AND ")
2315    }
2316
2317    /// **`ON CONFLICT (key) DO NOTHING`** — for the platforms that let the
2318    /// engine decide, which is every platform that has the clause.
2319    ///
2320    /// The engine tests the key as it writes, so two sessions registering one
2321    /// file at the same moment settle on one row rather than on whichever of
2322    /// them looked first.
2323    #[must_use]
2324    pub fn on_conflict_do_nothing(&self) -> String {
2325        format!(
2326            "{}\n{}\nON CONFLICT ({}) DO NOTHING;",
2327            self.target(),
2328            self.values_clause(),
2329            self.key_columns.join(", ")
2330        )
2331    }
2332
2333    /// **`ON DUPLICATE KEY UPDATE key = key`** — the `MySQL` family's no-op.
2334    ///
2335    /// `INSERT IGNORE` would be shorter and it is not the same request: it
2336    /// downgrades *every* error on the statement to a warning — a truncated
2337    /// value, a bad date — so a row that should have been refused arrives
2338    /// mangled and silent. Assigning the key to itself touches only the
2339    /// collision this is about, and leaves everything else to complain
2340    /// normally.
2341    #[must_use]
2342    pub fn on_duplicate_key_no_op(&self) -> String {
2343        let held = self
2344            .key_columns
2345            .iter()
2346            .map(|column| format!("{column} = {column}"))
2347            .collect::<Vec<_>>()
2348            .join(", ");
2349        format!("{}\n{}\nON DUPLICATE KEY UPDATE {held};", self.target(), self.values_clause())
2350    }
2351
2352    /// **The guarded form over an explicit one-row source** — the same
2353    /// `INSERT … SELECT … WHERE NOT EXISTS` the portable default writes, with a
2354    /// `FROM` clause under it.
2355    ///
2356    /// `GoogleSQL` refuses a `WHERE` on a query that selects from nothing —
2357    /// *"Query without FROM clause cannot have a WHERE clause"*, measured on the
2358    /// live service on every generated `register_file` procedure — while
2359    /// `PostgreSQL` and its cousins accept the bare form. `(SELECT 1)` is a
2360    /// one-row table that changes no result: the guard still decides whether
2361    /// the row is written, and the values are still the caller's.
2362    ///
2363    /// It stays a separate form rather than becoming the default because the
2364    /// engines that need no `FROM` read better without one, and a clause every
2365    /// platform carries for one platform's sake is how a rendering stops saying
2366    /// what it means.
2367    #[must_use]
2368    pub fn guarded_over_one_row(&self) -> String {
2369        format!(
2370            "{}\nSELECT {}\nFROM (SELECT 1)\nWHERE NOT EXISTS (SELECT 1 FROM {} WHERE {});",
2371            self.target(),
2372            self.values.join(", "),
2373            self.table,
2374            self.key_predicate("")
2375        )
2376    }
2377
2378    /// `INSERT INTO t (a, b)` — shared by every form below.
2379    fn target(&self) -> String {
2380        format!("INSERT INTO {} ({})", self.table, self.columns.join(", "))
2381    }
2382
2383    /// `VALUES (…)`.
2384    fn values_clause(&self) -> String {
2385        format!("VALUES ({})", self.values.join(", "))
2386    }
2387}
2388
2389/// Level 4 — DML rendering with reasonable defaults.
2390pub trait DmlRenderer: SqlRenderer {
2391    /// Prefix for INSERT statements.
2392    fn render_insert_prefix(&self) -> &'static str {
2393        "INSERT INTO"
2394    }
2395
2396    /// **Write one row, and do nothing if its key is already there.**
2397    ///
2398    /// Every platform can express this and they do not agree on how, so the
2399    /// spelling belongs to the dialect and not to the caller. The default is
2400    /// the portable one — an `INSERT … SELECT` guarded by `WHERE NOT EXISTS` —
2401    /// which needs no upsert syntax at all and is therefore the right answer
2402    /// for a platform nobody has written an arm for. Platforms with a native
2403    /// idiom override it: `ON CONFLICT … DO NOTHING` where that exists,
2404    /// `ON DUPLICATE KEY UPDATE` on the `MySQL` family.
2405    ///
2406    /// **No dialect is left without an idempotent form.** The guarded form is
2407    /// weaker than the native ones under concurrency — two sessions can both
2408    /// find the row absent and both insert — which is why a platform that can
2409    /// let the engine decide should, and why this one leans on the key's own
2410    /// uniqueness constraint to catch the race rather than pretending it cannot
2411    /// happen. On an engine that enforces no key at all the guard is the whole
2412    /// of what there is, and it is worth saying so rather than implying more.
2413    fn render_insert_if_absent(&self, insert: &InsertIfAbsent<'_>) -> String {
2414        format!(
2415            "{}\nSELECT {}\nWHERE NOT EXISTS (SELECT 1 FROM {} WHERE {});",
2416            insert.target(),
2417            insert.values.join(", "),
2418            insert.table,
2419            insert.key_predicate("")
2420        )
2421    }
2422
2423    /// Prefix for MERGE statements.
2424    fn render_merge_prefix(&self) -> &'static str {
2425        "MERGE INTO"
2426    }
2427
2428    /// Render the platform's upsert strategy keyword.
2429    ///
2430    /// Returns `None` if platform doesn't support upsert natively.
2431    fn render_upsert_strategy(&self) -> Option<&str> {
2432        None
2433    }
2434
2435    /// Render RETURNING / OUTPUT clause for INSERT/UPDATE.
2436    fn render_returning_clause(&self, _columns: &[&str]) -> Option<String> {
2437        None
2438    }
2439
2440    /// Render a LIMIT clause.
2441    fn render_limit_clause(&self, n: usize) -> String {
2442        format!("LIMIT {n}")
2443    }
2444
2445    /// Render a temporary table prefix or keyword.
2446    fn render_temp_table_prefix(&self) -> &'static str {
2447        "TEMPORARY"
2448    }
2449
2450    /// **One row holding an array becomes one row per element.**
2451    ///
2452    /// Razeri hands over the relation it is reading, the array column, and the
2453    /// element fields the statement needs; the engine answers with the `FROM`
2454    /// item that produces the rows and the expressions that read them. The
2455    /// three parts come back together because on at least one engine they
2456    /// depend on each other: its field expressions name an alias its own
2457    /// `FROM` item introduced.
2458    ///
2459    /// **The ordinal is one for the first element, on every engine.** Engines
2460    /// disagree about where an array starts — some count from one, some from
2461    /// zero, one answers in text — and the element's surrogate key is derived
2462    /// from its position. An engine answering in its own base would give one
2463    /// element two different keys depending on where it was stored, so the
2464    /// normalization belongs here, where the engine that counts from zero pays
2465    /// for it with one addition.
2466    ///
2467    /// `None` — the default — is this engine stating no flatten. Razeri then
2468    /// refuses the declaration and names the engine, rather than guessing an
2469    /// idiom or emitting a statement that does not run.
2470    fn render_flatten(&self, _request: &FlattenRequest<'_>) -> Option<Flatten> {
2471        None
2472    }
2473}
2474
2475/// Level 5 — Stored procedure / function rendering with defaults.
2476pub trait ProcedureRenderer: SqlRenderer {
2477    /// Prefix for CREATE PROCEDURE.
2478    fn render_create_procedure_prefix(&self) -> &'static str {
2479        "CREATE OR REPLACE PROCEDURE"
2480    }
2481
2482    /// Prefix for CREATE FUNCTION.
2483    fn render_create_function_prefix(&self) -> &'static str {
2484        "CREATE OR REPLACE FUNCTION"
2485    }
2486
2487    /// Render a variable declaration inside a procedure body.
2488    fn render_variable_declaration(&self, name: &str, dt: &DataType) -> String {
2489        format!("DECLARE {} {};", name, self.render_data_type(dt))
2490    }
2491
2492    /// Render BEGIN/END block delimiters.
2493    fn render_begin_end(&self, body: &str) -> String {
2494        format!("BEGIN\n{body}\nEND;")
2495    }
2496
2497    /// Whether procedures use a language clause (e.g., Snowflake JavaScript).
2498    fn requires_language_clause(&self) -> bool {
2499        false
2500    }
2501}
2502
2503#[cfg(test)]
2504mod tests {
2505    use super::*;
2506    use crate::DialectCapabilities;
2507    use crate::sid_spelling::SidHelperLanguage;
2508
2509    /// A test dialect that implements only the required `SqlRenderer` methods.
2510    /// All other traits use defaults.
2511    struct TestDialect;
2512
2513    impl SqlRenderer for TestDialect {
2514        fn render_data_type(&self, dt: &DataType) -> String {
2515            match dt {
2516                DataType::Boolean(_) => "BOOLEAN".to_string(),
2517                DataType::Integer(_) => "INTEGER".to_string(),
2518                _ => "TEXT".to_string(),
2519            }
2520        }
2521        fn quote_identifier(&self, name: &str) -> String {
2522            format!("\"{name}\"")
2523        }
2524        fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
2525            match schema {
2526                Some(s) => format!("{}.{}", self.quote_identifier(s), self.quote_identifier(name)),
2527                None => self.quote_identifier(name),
2528            }
2529        }
2530        fn render_current_timestamp(&self) -> String {
2531            "CURRENT_TIMESTAMP".to_string()
2532        }
2533    }
2534
2535    impl ExpressionRenderer for TestDialect {}
2536    impl DdlRenderer for TestDialect {}
2537    impl DmlRenderer for TestDialect {}
2538    impl ProcedureRenderer for TestDialect {}
2539    impl DialectCapabilities for TestDialect {}
2540
2541    /// The same dialect with the two file-and-key facts stated. It exists to
2542    /// prove that a dialect Razeri never compiled against reaches the behaviour
2543    /// by *declaring* it — which is the whole property, and is not visible from
2544    /// a dialect that ships in this tree.
2545    struct StatedDialect;
2546
2547    impl SqlRenderer for StatedDialect {
2548        fn render_data_type(&self, _dt: &DataType) -> String {
2549            "TEXT".to_string()
2550        }
2551        fn quote_identifier(&self, name: &str) -> String {
2552            format!("\"{name}\"")
2553        }
2554        fn qualify_name(&self, _schema: Option<&str>, name: &str) -> String {
2555            self.quote_identifier(name)
2556        }
2557        fn render_current_timestamp(&self) -> String {
2558            "CURRENT_TIMESTAMP".to_string()
2559        }
2560        fn sid_helper_body(&self) -> Option<SidHelperBody> {
2561            Some(SidHelperBody {
2562                language: SidHelperLanguage::Named("ecmascript"),
2563                body: "return 1;",
2564            })
2565        }
2566        fn delimited_file_relation(&self, path_literal: &str) -> Option<String> {
2567            Some(format!("SCAN({path_literal})"))
2568        }
2569    }
2570
2571    /// **Silence is the default, and silence means Razeri composes.**
2572    ///
2573    /// Both facts are opt-in: a dialect that says nothing gets the SQL
2574    /// derivation for its key and no generated staging fill at all. Neither
2575    /// default may become a guess — a composed body on an engine with no hash
2576    /// keys a warehouse to a number nothing reproduces, and an invented file
2577    /// relation fails the load on its first statement.
2578    #[test]
2579    fn a_dialect_that_states_neither_fact_is_asked_for_nothing() {
2580        let d: &dyn SqlRenderer = &TestDialect;
2581        assert_eq!(d.sid_helper_body(), None);
2582        assert_eq!(d.delimited_file_relation("'/data/x.csv'"), None);
2583    }
2584
2585    /// And a dialect that states them is heard through the trait object, which
2586    /// is the only way Razeri ever holds one.
2587    #[test]
2588    fn a_dialect_that_states_them_is_heard_through_the_trait_object() {
2589        let d: &dyn SqlRenderer = &StatedDialect;
2590        assert_eq!(
2591            d.sid_helper_body(),
2592            Some(SidHelperBody {
2593                language: SidHelperLanguage::Named("ecmascript"),
2594                body: "return 1;",
2595            })
2596        );
2597        assert_eq!(
2598            d.delimited_file_relation("'/data/x.csv'").as_deref(),
2599            Some("SCAN('/data/x.csv')")
2600        );
2601    }
2602
2603    #[test]
2604    fn sql_renderer_core_methods_work() {
2605        let d = TestDialect;
2606        assert_eq!(d.render_data_type(&DataType::boolean()), "BOOLEAN");
2607        assert_eq!(d.quote_identifier("users"), "\"users\"");
2608        assert_eq!(d.qualify_name(Some("public"), "users"), "\"public\".\"users\"");
2609        assert_eq!(d.render_current_timestamp(), "CURRENT_TIMESTAMP");
2610    }
2611
2612    #[test]
2613    fn dialect_name_returns_ansi_by_default() {
2614        let d = TestDialect;
2615        assert_eq!(d.dialect_name(), "ansi");
2616    }
2617
2618    #[test]
2619    fn expression_renderer_defaults_produce_ansi_sql() {
2620        let d = TestDialect;
2621        assert_eq!(d.render_cast("age", &DataType::int()), "CAST(age AS INTEGER)");
2622        assert_eq!(d.render_concat(&["a".into(), "b".into()]), "CONCAT(a, b)");
2623        assert_eq!(d.render_coalesce(&["x".into(), "y".into()]), "COALESCE(x, y)");
2624    }
2625
2626    #[test]
2627    fn ddl_renderer_defaults_produce_ansi_sql() {
2628        let d = TestDialect;
2629        assert_eq!(d.render_create_table_prefix(), "CREATE TABLE");
2630        assert_eq!(d.render_create_view_prefix(), "CREATE OR REPLACE VIEW");
2631        assert_eq!(
2632            d.render_column_definition("name", &DataType::boolean(), false),
2633            "\"name\" BOOLEAN NOT NULL"
2634        );
2635        assert_eq!(d.render_column_definition("age", &DataType::int(), true), "\"age\" INTEGER");
2636    }
2637
2638    #[test]
2639    fn dml_renderer_defaults_produce_ansi_sql() {
2640        let d = TestDialect;
2641        assert_eq!(d.render_insert_prefix(), "INSERT INTO");
2642        assert_eq!(d.render_merge_prefix(), "MERGE INTO");
2643        assert_eq!(d.render_limit_clause(10), "LIMIT 10");
2644        assert!(d.render_upsert_strategy().is_none());
2645        assert!(d.render_returning_clause(&["id"]).is_none());
2646    }
2647
2648    #[test]
2649    fn procedure_renderer_defaults_produce_ansi_sql() {
2650        let d = TestDialect;
2651        assert_eq!(d.render_create_procedure_prefix(), "CREATE OR REPLACE PROCEDURE");
2652        assert_eq!(
2653            d.render_variable_declaration("v_count", &DataType::int()),
2654            "DECLARE v_count INTEGER;"
2655        );
2656        assert!(!d.requires_language_clause());
2657    }
2658
2659    #[test]
2660    fn trait_hierarchy_allows_selective_override() {
2661        struct MySqlLike;
2662        impl SqlRenderer for MySqlLike {
2663            fn render_data_type(&self, _: &DataType) -> String {
2664                "INT".into()
2665            }
2666            fn quote_identifier(&self, name: &str) -> String {
2667                format!("`{name}`")
2668            }
2669            fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
2670                match schema {
2671                    Some(s) => format!("`{s}`.`{name}`"),
2672                    None => format!("`{name}`"),
2673                }
2674            }
2675            fn render_current_timestamp(&self) -> String {
2676                "NOW()".into()
2677            }
2678        }
2679        impl ExpressionRenderer for MySqlLike {
2680            fn render_concat(&self, parts: &[String]) -> String {
2681                format!("CONCAT({})", parts.join(", "))
2682            }
2683        }
2684        impl DdlRenderer for MySqlLike {}
2685        impl DmlRenderer for MySqlLike {}
2686        impl ProcedureRenderer for MySqlLike {}
2687        impl DialectCapabilities for MySqlLike {}
2688
2689        let d = MySqlLike;
2690        assert_eq!(d.quote_identifier("users"), "`users`");
2691        assert_eq!(d.render_current_timestamp(), "NOW()");
2692        // DDL defaults still work even though we only overrode expressions
2693        assert_eq!(d.render_create_table_prefix(), "CREATE TABLE");
2694    }
2695
2696    #[test]
2697    fn sql_renderer_expression_extensions_produce_ansi_defaults() {
2698        let d = TestDialect;
2699        assert_sql_renderer_literal_defaults(&d);
2700        assert_sql_renderer_parameter_and_merge_defaults(&d);
2701        assert_sql_renderer_update_insert_defaults(&d);
2702        assert_sql_renderer_partition_and_window_defaults(&d);
2703    }
2704
2705    /// Assert ANSI defaults for literal, date/time, and basic expression hooks.
2706    /// Parameters:
2707    /// - dialect: Renderer under test.
2708    fn assert_sql_renderer_literal_defaults(dialect: &TestDialect) {
2709        assert_eq!(dialect.render_boolean_literal(true), "TRUE");
2710        assert_eq!(dialect.render_boolean_literal(false), "FALSE");
2711        assert_eq!(dialect.render_date_literal("2024-01-15"), "DATE '2024-01-15'");
2712        assert_eq!(
2713            dialect.render_timestamp_literal("2024-01-15 12:00:00"),
2714            "TIMESTAMP '2024-01-15 12:00:00'"
2715        );
2716        assert_eq!(dialect.render_interval_literal("1 year"), "INTERVAL '1 year'");
2717        assert_eq!(dialect.render_current_date(), "CURRENT_DATE");
2718        assert_eq!(dialect.concat_operator(), "||");
2719        assert_eq!(dialect.render_nan_literal(&DataType::float()), "NULL");
2720        assert_eq!(dialect.render_infinity_literal(true, &DataType::float()), "NULL");
2721        assert_eq!(dialect.ifnull_function_name(), "COALESCE");
2722        assert_eq!(
2723            dialect.render_iif("x > 0", "pos", "neg"),
2724            "CASE WHEN x > 0 THEN pos ELSE neg END"
2725        );
2726        assert_eq!(
2727            dialect.render_date_trunc("month", "created_at"),
2728            "DATE_TRUNC('month', created_at)"
2729        );
2730        assert_eq!(
2731            dialect.render_date_extract("year", "created_at"),
2732            "EXTRACT(year FROM created_at)"
2733        );
2734        assert_eq!(dialect.render_try_cast("age", &DataType::int()), "TRY_CAST(age AS INTEGER)");
2735    }
2736
2737    /// Assert ANSI defaults for parameter and merge-related extension hooks.
2738    /// Parameters:
2739    /// - dialect: Renderer under test.
2740    fn assert_sql_renderer_parameter_and_merge_defaults(dialect: &TestDialect) {
2741        assert_eq!(dialect.render_parameter_placeholder(1), "$1");
2742        assert_eq!(dialect.render_named_parameter("user_id"), ":user_id");
2743        assert_eq!(SqlRenderer::merge_not_matched_keyword(dialect), "WHEN NOT MATCHED");
2744        assert!(!SqlRenderer::supports_merge_not_matched_by_source(dialect));
2745        assert!(SqlRenderer::merge_supported(dialect));
2746        assert!(!SqlRenderer::merge_supports_schema_evolution_keyword(dialect));
2747        assert!(!SqlRenderer::merge_supports_table_hints(dialect));
2748        assert!(!SqlRenderer::merge_uses_output_clause(dialect));
2749        assert!(!SqlRenderer::merge_supports_returning_clause(dialect));
2750        assert_eq!(dialect.schema_separator(), ".");
2751        assert_eq!(dialect.statement_separator(), ";\n");
2752    }
2753
2754    /// Assert ANSI defaults for update and insert behavior extension hooks.
2755    /// Parameters:
2756    /// - dialect: Renderer under test.
2757    fn assert_sql_renderer_update_insert_defaults(dialect: &TestDialect) {
2758        assert!(!SqlRenderer::supports_compound_assignment(dialect));
2759        assert!(!SqlRenderer::supports_update_from(dialect));
2760        assert!(!SqlRenderer::uses_alter_table_update(dialect));
2761        // How a mutation is SPELLED and what it may LOOK AT are
2762        // two facts, and the ANSI default is that a mutation may correlate.
2763        assert!(!SqlRenderer::mutation_reads_only_its_own_rows(dialect));
2764        assert!(!dialect.update_supports_top_clause());
2765        assert!(!dialect.update_supports_table_hints());
2766        assert!(!dialect.update_uses_output_clause());
2767        assert!(!dialect.update_supports_returning());
2768        assert!(!dialect.insert_uses_output_clause());
2769        assert!(!dialect.insert_supports_returning());
2770        assert!(!dialect.insert_supports_table_hints());
2771        assert!(!SqlRenderer::supports_insert_overwrite(dialect));
2772        assert!(!SqlRenderer::supports_on_conflict(dialect));
2773        assert!(!dialect.supports_overriding_clause());
2774        assert_eq!(dialect.returning_clause_keyword(), "RETURNING");
2775        assert!(!SqlRenderer::supports_returning_into(dialect));
2776        assert!(!SqlRenderer::supports_multi_table_insert(dialect));
2777    }
2778
2779    /// Assert ANSI defaults for partitioning, generated columns, and window hooks.
2780    /// Parameters:
2781    /// - dialect: Renderer under test.
2782    fn assert_sql_renderer_partition_and_window_defaults(dialect: &TestDialect) {
2783        assert_eq!(
2784            dialect.render_time_partition_expr("event_ts", "monthly"),
2785            "date_trunc('month', event_ts)"
2786        );
2787        assert_eq!(dialect.render_partition_clause("event_ts"), "PARTITION BY (event_ts)");
2788        assert_eq!(
2789            dialect.render_generated_column("event_month", "DATE_TRUNC('month', event_ts)"),
2790            "event_month GENERATED ALWAYS AS (DATE_TRUNC('month', event_ts)) STORED"
2791        );
2792        assert!(!SqlRenderer::supports_window_frame_exclude(dialect));
2793        assert!(!SqlRenderer::supports_window_frame_groups(dialect));
2794        assert!(SqlRenderer::supports_named_windows(dialect));
2795    }
2796
2797    #[test]
2798    fn ddl_renderer_extensions_produce_ansi_defaults() {
2799        let d = TestDialect;
2800        assert_ddl_renderer_transaction_and_view_defaults(&d);
2801        assert_ddl_renderer_drop_defaults(&d);
2802    }
2803
2804    /// Assert ANSI DDL defaults for transaction and view keyword hooks.
2805    /// Parameters:
2806    /// - dialect: Renderer under test.
2807    fn assert_ddl_renderer_transaction_and_view_defaults(dialect: &TestDialect) {
2808        assert_eq!(dialect.render_begin_transaction(), "BEGIN");
2809        assert_eq!(dialect.render_commit(), "COMMIT");
2810        assert!(dialect.supports_ddl_transactions());
2811        assert_eq!(dialect.render_create_view_keyword(false, false), "CREATE OR REPLACE VIEW");
2812        assert_eq!(
2813            dialect.render_create_view_keyword(true, false),
2814            "CREATE OR REPLACE SECURE VIEW"
2815        );
2816        assert!(dialect.supports_alter_drop_if_exists());
2817    }
2818
2819    /// Assert ANSI DDL defaults for drop-related behavior hooks.
2820    /// Parameters:
2821    /// - dialect: Renderer under test.
2822    fn assert_ddl_renderer_drop_defaults(dialect: &TestDialect) {
2823        assert_eq!(dialect.render_drop_object_keyword("table"), "TABLE");
2824        assert_eq!(
2825            dialect.render_drop_function("my_func", &[], true),
2826            "DROP FUNCTION IF EXISTS my_func"
2827        );
2828        assert!(!dialect.drop_index_uses_on_syntax());
2829        assert!(DdlRenderer::supports_cascade_drop(dialect));
2830        assert!(dialect.supports_drop_function_arg_types());
2831        assert!(!DdlRenderer::supports_sync_drop(dialect));
2832        assert!(!DdlRenderer::supports_force_drop(dialect));
2833        assert!(!dialect.supports_alter_drop_cascade());
2834    }
2835
2836    /// **The one statement every engine takes**, written for a dialect that
2837    /// answered nothing. A plugin author gets the commonest evolution there is
2838    /// without writing a line for it.
2839    #[test]
2840    fn a_dialect_that_answers_nothing_still_adds_a_column() {
2841        let data_type = DataType::boolean();
2842        let written = TestDialect.render_column_evolution(&ColumnEvolution::AddColumn {
2843            schema: Some("telemetry_core"),
2844            table: "clients_daily",
2845            column: "is_default_browser",
2846            data_type: &data_type,
2847        });
2848        assert_eq!(
2849            written.as_deref(),
2850            Some(
2851                r#"ALTER TABLE "telemetry_core"."clients_daily" ADD COLUMN "is_default_browser" BOOLEAN"#
2852            )
2853        );
2854    }
2855
2856    /// Everything else waits to be spelled. A default picked from one engine's
2857    /// grammar is a statement the others reject, handed to an operator who
2858    /// finds that out against their own warehouse.
2859    #[test]
2860    fn nothing_else_is_spelled_until_a_dialect_spells_it() {
2861        let data_type = DataType::text();
2862        assert!(
2863            TestDialect
2864                .render_column_evolution(&ColumnEvolution::WidenColumn {
2865                    schema: None,
2866                    table: "t",
2867                    column: "c",
2868                    data_type: &data_type,
2869                })
2870                .is_none()
2871        );
2872        assert!(
2873            TestDialect
2874                .render_column_evolution(&ColumnEvolution::AddNestedField {
2875                    schema: None,
2876                    table: "t",
2877                    column: "c",
2878                    field: "f",
2879                    data_type: &data_type,
2880                })
2881                .is_none()
2882        );
2883    }
2884}