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