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