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