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