Skip to main content

razeri_plugin_contract/
renderer.rs

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