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