Skip to main content

razeri_plugin_contract/
renderer.rs

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