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