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