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