razeri_plugin_contract/capabilities.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//! Boolean capability flags with conservative defaults.
6//!
7//! [`DialectCapabilities`] tells the rendering layer what syntax features
8//! the platform supports. Defaults are conservative (`false` / ANSI-safe),
9//! so unknown platforms produce safe SQL.
10
11use crate::data_type::DataType;
12use crate::engine_version::EngineVersion;
13use crate::evolution::EvolutionAnswers;
14use crate::merge_shape::MergeShape;
15
16/// **What one engine holds in a text column**: the longest `varchar(n)` it
17/// takes, how that length is counted, where the number came from, and what an
18/// unbounded `text` column cannot be there.
19///
20/// Answered by [`DialectCapabilities::text_limits`]. A length past the
21/// longest is refused by name before anything is rendered, as a decimal wider
22/// than the engine holds is.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[non_exhaustive]
25pub struct TextLimits {
26 /// The longest `varchar(n)` a column takes, where the engine has one.
27 pub longest_varchar: Option<u32>,
28 /// How [`Self::longest_varchar`] is counted.
29 pub counted_in: VarcharUnit,
30 /// Where the number came from.
31 pub source: LimitSource,
32 /// What the engine refuses an unbounded `text` column as, where it
33 /// refuses it as a key — `a key or UNIQUE`, `a primary key`.
34 pub text_is_not: Option<&'static str>,
35}
36
37impl TextLimits {
38 /// A longest `varchar`, counted in `counted_in`, from `source`.
39 #[must_use]
40 pub const fn new(longest_varchar: u32, counted_in: VarcharUnit, source: LimitSource) -> Self {
41 Self { longest_varchar: Some(longest_varchar), counted_in, source, text_is_not: None }
42 }
43
44 /// The same limits, for an engine that refuses an unbounded `text` as
45 /// `what` — `a key or UNIQUE`.
46 #[must_use]
47 pub const fn and_text_is_not(self, what: &'static str) -> Self {
48 Self { text_is_not: Some(what), ..self }
49 }
50
51 /// Why `varchar(length)` does not fit, in a few plain words, or `None`
52 /// when it does.
53 #[must_use]
54 pub fn misfit(&self, length: u32) -> Option<String> {
55 let longest = self.longest_varchar?;
56 (length > longest).then(|| {
57 format!(
58 "its varchar holds at most {longest} {}, and this asks for {length}",
59 self.counted_in.word()
60 )
61 })
62 }
63}
64
65/// **How an engine counts a `varchar`'s declared length.**
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67#[non_exhaustive]
68pub enum VarcharUnit {
69 /// In characters, whatever each takes to store.
70 Characters,
71 /// In bytes: a character outside ASCII takes more than one.
72 Bytes,
73}
74
75impl VarcharUnit {
76 /// The unit as a sentence says it: `characters`, `bytes`.
77 #[must_use]
78 pub const fn word(self) -> &'static str {
79 match self {
80 Self::Characters => "characters",
81 Self::Bytes => "bytes",
82 }
83 }
84}
85
86/// **Where a stated limit came from.**
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88#[non_exhaustive]
89pub enum LimitSource {
90 /// Measured on the engine: one length past it refused at `CREATE TABLE`.
91 MeasuredOnTheEngine,
92 /// Read from the engine's documentation; no engine was run.
93 TheEnginesDocumentation,
94}
95
96/// **The widest exact decimal one engine's column holds.**
97///
98/// Every engine that records one refuses a precision of zero, so the fewest
99/// digits is always one and is not a field.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct DecimalLimits {
102 /// The most digits a column may declare.
103 pub max_precision: u16,
104 /// The most digits after the decimal point.
105 pub max_scale: u16,
106 /// Whether the digits after the point must be no more than the digits in
107 /// all. Some engines let a column hold only a fraction smaller than a
108 /// thousandth, with more scale than precision; most refuse it.
109 pub scale_within_precision: bool,
110 /// The most digits before the decimal point, where the engine bounds
111 /// them apart from the digits in all; `None` where the digits in all are
112 /// the only bound. `BigQuery` holds 76 digits, but never more than 38 of
113 /// them before the point.
114 pub max_integer_digits: Option<u16>,
115}
116
117impl DecimalLimits {
118 /// Why `decimal(precision, scale)` does not fit, in a few plain words, or
119 /// `None` when it does.
120 ///
121 /// Parameters:
122 /// - `precision`: the declared digits in all;
123 /// - `scale`: the declared digits after the point.
124 #[must_use]
125 pub fn misfit(&self, precision: u16, scale: u16) -> Option<String> {
126 let most = self.max_precision;
127 if precision == 0 || precision > most {
128 return Some(format!(
129 "its decimals hold 1 to {most} digits, and this asks for {precision}"
130 ));
131 }
132 if scale > self.max_scale {
133 return Some(format!(
134 "its decimals hold at most {} digits after the point, and this asks for {scale}",
135 self.max_scale
136 ));
137 }
138 if self.scale_within_precision && scale > precision {
139 return Some(format!(
140 "its decimals hold no more digits after the point than in all, and this asks for \
141 {scale} of {precision}"
142 ));
143 }
144 let before_point = precision.saturating_sub(scale);
145 if let Some(most) = self.max_integer_digits
146 && before_point > most
147 {
148 return Some(format!(
149 "its decimals hold at most {most} digits before the point, and this asks for \
150 {before_point}"
151 ));
152 }
153 None
154 }
155
156 /// The nearest `decimal(precision, scale)` these limits hold, for a
157 /// refusal to suggest in place of the one declared.
158 ///
159 /// Parameters:
160 /// - `precision`: the declared digits in all;
161 /// - `scale`: the declared digits after the point.
162 #[must_use]
163 pub fn nearest(&self, precision: u16, scale: u16) -> (u16, u16) {
164 let mut scale = scale.min(self.max_scale);
165 let mut precision = precision.clamp(1, self.max_precision);
166 if self.scale_within_precision {
167 scale = scale.min(precision);
168 }
169 if let Some(most) = self.max_integer_digits {
170 precision = precision.min(scale + most).max(1);
171 }
172 if self.scale_within_precision {
173 scale = scale.min(precision);
174 }
175 (precision, scale)
176 }
177}
178
179/// Level 6 — Boolean capability flags with conservative defaults.
180///
181/// These flags tell the rendering layer what syntax features the platform supports.
182/// Defaults are `false` / conservative, so unknown platforms produce safe ANSI SQL.
183pub trait DialectCapabilities {
184 // ═══════════════════════════════════════════════════════════════
185 // DML Capabilities
186 // ═══════════════════════════════════════════════════════════════
187
188 /// **How this engine performs the upsert Razeri's generated load is.**
189 ///
190 /// See [`MergeShape`]. The default is a refusal, because the honest answer
191 /// for an engine nobody has measured is that Razeri does not know how to
192 /// load it — not that it spells its upsert the way `MySQL` does.
193 fn merge_shape(&self) -> MergeShape {
194 MergeShape::None
195 }
196
197 /// **How much of the deployed schema a reconciliation can compare with
198 /// the design here.**
199 ///
200 /// Razeri's schema check reads the engine's catalogue in one query: the
201 /// ANSI `information_schema.columns`, a `FULL OUTER JOIN` against the
202 /// design's own columns, and a table value constructor with a derived
203 /// column list, `(VALUES ('a'), ('b')) AS designed (column_name)`. See
204 /// [`CatalogueReach`]. The default is
205 /// [`CatalogueReach::Nothing`] with a sentence saying Razeri has not
206 /// written this engine's catalogue query: an engine nobody has measured
207 /// gets no check, and a warning that says why, rather than a query that
208 /// fails.
209 fn catalogue_reach(&self) -> CatalogueReach {
210 CatalogueReach::Nothing(
211 "razeri has not written this platform's catalog query, and guessing at one would \
212 emit SQL that fails rather than a verdict",
213 )
214 }
215
216 /// **What this engine keeps when a table that already holds rows changes
217 /// shape.**
218 ///
219 /// See [`EvolutionAnswers`]. The default answers nothing, which Razeri
220 /// reads as *nobody has measured this engine* — so a change Razeri cannot
221 /// otherwise prove keeps every value is reported as one that does not, and
222 /// an operator gets a warning they can read instead of a surprise.
223 fn evolution_answers(&self) -> EvolutionAnswers {
224 EvolutionAnswers::none()
225 }
226
227 /// Whether the platform supports INSERT ... ON CONFLICT (PostgreSQL-style upsert).
228 fn supports_on_conflict(&self) -> bool {
229 false
230 }
231
232 /// Whether the platform supports INSERT OVERWRITE.
233 fn supports_insert_overwrite(&self) -> bool {
234 false
235 }
236
237 /// Whether the platform supports UPDATE ... FROM.
238 fn supports_update_from(&self) -> bool {
239 false
240 }
241
242 /// Whether the platform supports RETURNING / OUTPUT clauses.
243 fn supports_returning(&self) -> bool {
244 false
245 }
246
247 // ═══════════════════════════════════════════════════════════════
248 // The server's version
249 // ═══════════════════════════════════════════════════════════════
250
251 /// **How to ask a running server its version**, or `None` where this
252 /// engine has no version a user chooses, or nobody has written the
253 /// question.
254 ///
255 /// The query answers one row whose first word is the version in the
256 /// spelling this dialect declares (`17`, `8.4`, `23ai`); anything after it
257 /// is the server's own words, which a refusal prints. A generated deploy
258 /// script sends it after login and before the schema, and refuses a server
259 /// older than the version the bundle was rendered for, naming both. The
260 /// default is `None`, and a script that has no question asks none.
261 fn server_version_query(&self) -> Option<&str> {
262 None
263 }
264
265 /// **The version in one answer to [`Self::server_version_query`]**, or
266 /// `None` when the answer holds none.
267 ///
268 /// Parameters:
269 /// - `answer`: the one row the query answered, as text.
270 ///
271 /// The default reads the first word, which is what a deploy script reads
272 /// too; override it only to read more of the answer than that.
273 fn parse_server_version(&self, answer: &str) -> Option<EngineVersion> {
274 answer.split_whitespace().next().and_then(|word| EngineVersion::parse(word).ok())
275 }
276
277 // ═══════════════════════════════════════════════════════════════
278 // Query Capabilities
279 // ═══════════════════════════════════════════════════════════════
280
281 /// Whether the platform supports Common Table Expressions (WITH).
282 fn supports_cte(&self) -> bool {
283 true
284 }
285
286 /// Whether the platform supports recursive CTEs.
287 fn supports_recursive_cte(&self) -> bool {
288 true
289 }
290
291 /// Whether the platform supports window functions.
292 fn supports_window_functions(&self) -> bool {
293 true
294 }
295
296 /// Whether the platform supports named windows (WINDOW clause).
297 fn supports_named_windows(&self) -> bool {
298 true
299 }
300
301 /// Whether the platform supports QUALIFY clause.
302 fn supports_qualify(&self) -> bool {
303 false
304 }
305
306 /// Whether the platform supports GROUPS frame type in window functions.
307 fn supports_groups_frame(&self) -> bool {
308 false
309 }
310
311 /// Whether the platform supports NULLS FIRST/LAST in ORDER BY.
312 fn supports_nulls_ordering(&self) -> bool {
313 true
314 }
315
316 // ═══════════════════════════════════════════════════════════════
317 // DDL Capabilities
318 // ═══════════════════════════════════════════════════════════════
319
320 /// Whether the platform supports IDENTITY columns.
321 fn supports_identity(&self) -> bool {
322 false
323 }
324
325 /// Whether the platform supports sequences (CREATE SEQUENCE).
326 fn supports_sequences(&self) -> bool {
327 false
328 }
329
330 /// Whether the platform supports schema namespacing.
331 fn supports_schemas(&self) -> bool {
332 true
333 }
334
335 /// **Can a routine created in one schema be called from a routine created
336 /// in another, with nothing else done?**
337 ///
338 /// True nearly everywhere, and true by default: a schema on most engines is
339 /// a namespace, and naming an object in another namespace is all that
340 /// reaching it takes.
341 ///
342 /// Answer `false` where a schema is also an owner and reaching across one
343 /// costs a privilege granted per object. On Oracle a schema IS a user; a
344 /// routine runs with its owner's privileges, so a function created in
345 /// `acme_core` calling one created in `acme_raw` compiles with errors —
346 /// `ORA-60535: invalid identifier`, measured — however exactly it spells
347 /// the name.
348 ///
349 /// What Razeri does with the answer is put a copy of the key function in
350 /// **every namespace that calls it** instead of one per domain, so each
351 /// namespace stands on its own and no deploy owes a grant. It is one
352 /// derivation either way: the copies are the same builder over the same
353 /// vocabulary, not two spellings free to drift. The engine-level hash
354 /// beneath them keeps its single home — see
355 /// [`ReplicationRenderer::sid_prerequisite_home`](crate::ReplicationRenderer::sid_prerequisite_home)
356 /// — because that one is Razeri's own object and installs its own grant.
357 fn routine_is_callable_across_schemas(&self) -> bool {
358 true
359 }
360
361 /// Whether the platform supports bulk load (COPY, BULK INSERT, etc.).
362 fn supports_bulk_load(&self) -> bool {
363 false
364 }
365
366 /// **Whether a generated load is written as a stored procedure** on this
367 /// engine.
368 ///
369 /// Razeri asks this before it writes a load as a procedure, and asks
370 /// [`SqlRenderer::procedure_form`](crate::SqlRenderer::procedure_form)
371 /// how to spell one. The default is `false`, and a load is then written as
372 /// plain statements. A dialect that answers `procedure_form` with
373 /// `ProcedureForm::Custom` answers `true` here, or its procedures are
374 /// never asked for; one that answers `ProcedureForm::Unsupported` leaves
375 /// this `false`. The conformance report holds the two to each other.
376 fn supports_stored_procedures(&self) -> bool {
377 false
378 }
379
380 /// Whether the platform **stores functions at all** — a routine the schema
381 /// creates and later calls by name, in any spelling.
382 ///
383 /// Nearly every engine razeri reaches has one: `CREATE FUNCTION` on
384 /// `PostgreSQL`, `MySQL`, `MariaDB`, Snowflake, `SQL Server` and Databricks,
385 /// a `MACRO` on `DuckDB`, a lambda `CREATE FUNCTION` on `ClickHouse`.
386 ///
387 /// **`SQLite` answers `false`, and it is the reason this question is asked
388 /// out loud.** `SQLite` has no `CREATE FUNCTION` statement in any spelling
389 /// and no `CREATE PROCEDURE` either — a function there is registered by the
390 /// program that opens the database, so nothing written into a schema script
391 /// can create one. Inheriting the `PostgreSQL` form wrote
392 /// `CREATE OR REPLACE FUNCTION … LANGUAGE SQL RETURN …` into `schema.sql`,
393 /// which `SQLite` answers with `near "CREATE": syntax error` part way
394 /// through the script, so the build refuses in front of the script instead.
395 ///
396 /// The default is `true`, matching [`Self::supports_table_functions`] and
397 /// for the same reason: refusing is the destructive answer, so a refusal
398 /// fires only where an engine is KNOWN to have no form, never because a
399 /// dialect forgot to answer. That reverses the conservative-`false` default
400 /// this trait uses for flags that *add* syntax — here `false` is the
401 /// unusual claim, and it is made deliberately, one dialect at a time.
402 fn supports_stored_functions(&self) -> bool {
403 true
404 }
405
406 /// Whether the platform has a **table-valued function** — a routine a query
407 /// selects FROM, returning columns rather than one value.
408 ///
409 /// Most engines have one in some spelling: `RETURNS TABLE(…)` on
410 /// `PostgreSQL`, Snowflake and Databricks, an inline TVF on SQL Server, a
411 /// table `MACRO` on `DuckDB`, a parameterized view on `ClickHouse`.
412 ///
413 /// **`MySQL` and `MariaDB` answer `false`, and they are the reason this
414 /// question exists.** A stored function there returns exactly one value;
415 /// there is no clause that returns a relation, in any spelling. Rendering
416 /// `RETURNS TABLE` for them produced `ERROR 1064 … syntax error` in the
417 /// middle of a schema script (measured on `MySQL` 8.4 and `MariaDB` 11.4),
418 /// so the build refuses in front of the script instead.
419 ///
420 /// The default is `true`: a refusal fires only where an engine is KNOWN to
421 /// have no form, never because a dialect forgot to answer.
422 fn supports_table_functions(&self) -> bool {
423 true
424 }
425
426 /// Whether the platform supports tasks / scheduled jobs.
427 fn supports_tasks(&self) -> bool {
428 false
429 }
430
431 /// Whether the platform supports native table partitioning.
432 fn supports_partitioning(&self) -> bool {
433 true
434 }
435
436 /// Whether the platform supports materialized views.
437 fn supports_materialized_views(&self) -> bool {
438 true
439 }
440
441 /// Whether the platform supports generated/computed columns.
442 fn supports_generated_columns(&self) -> bool {
443 true
444 }
445
446 /// Whether the platform supports JSONB or native JSON type.
447 fn supports_json(&self) -> bool {
448 true
449 }
450
451 // ═══════════════════════════════════════════════════════════════
452 // Type Capabilities
453 // ═══════════════════════════════════════════════════════════════
454
455 /// Whether the platform supports unsigned integers natively.
456 fn supports_unsigned_integers(&self) -> bool {
457 false
458 }
459
460 /// Whether the platform supports arrays natively.
461 fn supports_arrays(&self) -> bool {
462 false
463 }
464
465 /// **Whether this engine has a type of its own for a nested type, or
466 /// stores it as something else.**
467 ///
468 /// A nested column is an array, a map or a record. Some engines have all
469 /// three; some have one; some have none and hold the value as JSON or as
470 /// text. Razeri never refuses the column — the design is portable and it is
471 /// the storage that degrades — but it does say so once, naming the engine,
472 /// the columns and what each became.
473 ///
474 /// The default is `false`, which is the honest answer for an engine nobody
475 /// has answered for: the reader is told what the column became rather than
476 /// left to assume the type survived.
477 ///
478 /// Parameters:
479 /// - `data_type`: the nested type in question.
480 fn renders_nested_type_natively(&self, data_type: &DataType) -> bool {
481 let _ = data_type;
482 false
483 }
484
485 /// **Whether this engine can tell two values of a nested type apart.**
486 ///
487 /// Change detection asks whether an arriving value differs from the
488 /// standing one. For a scalar every engine answers with `<>`. For a nested
489 /// value they do not agree: some compare arrays, maps and records directly,
490 /// some refuse the comparison outright, and some hold the value as text and
491 /// compare the text. A column this returns `false` for is left out of
492 /// change detection and named in a note, rather than compared with an
493 /// operator the engine will reject.
494 ///
495 /// The default is `false` — an unanswered engine leaves the column
496 /// unwatched and says so, which is the failure that can be read.
497 ///
498 /// Parameters:
499 /// - `data_type`: the nested type in question.
500 fn compares_nested_values(&self, data_type: &DataType) -> bool {
501 let _ = data_type;
502 false
503 }
504
505 /// **Whether this engine can collapse rows on a nested column** — group by
506 /// it, or select distinct over it.
507 ///
508 /// A load reads its staged rows through one collapsed form so that a row
509 /// stated twice, identically, is one arriving row rather than a conflict.
510 /// The plain spelling of that collapse is `SELECT DISTINCT` over every
511 /// column, and grouping is what `DISTINCT` is: an engine that will not
512 /// group a value of a nested type will not take the column in a `DISTINCT`
513 /// either.
514 ///
515 /// It is a separate question from [`Self::compares_nested_values`], and the
516 /// two engines that answer them differently are the reason it is asked
517 /// separately: `BigQuery` compares two records with `=` and refuses to group
518 /// on one.
519 ///
520 /// Where the answer is `false` the collapse is written as a `GROUP BY` over
521 /// the other columns with [`crate::SqlRenderer::render_any_value`] around
522 /// this one.
523 ///
524 /// The default is `true`: `SELECT DISTINCT` over every column is the plain
525 /// spelling, and an engine says otherwise only where somebody has found
526 /// that it refuses.
527 ///
528 /// Asked only of a nested type — an array, a map or a record. The answer
529 /// for anything else is not read.
530 ///
531 /// Parameters:
532 /// - `data_type`: the nested type in question.
533 fn groups_nested_values(&self, data_type: &DataType) -> bool {
534 let _ = data_type;
535 true
536 }
537
538 /// Whether the platform supports native BOOLEAN type (vs BIT).
539 fn supports_boolean(&self) -> bool {
540 true
541 }
542
543 // ═══════════════════════════════════════════════════════════════
544 // Limits & Defaults
545 // ═══════════════════════════════════════════════════════════════
546
547 /// The default schema name for this platform.
548 fn default_schema(&self) -> &'static str {
549 "public"
550 }
551
552 /// Maximum identifier length for this platform.
553 fn max_identifier_length(&self) -> usize {
554 63
555 }
556
557 /// Maximum number of columns in a table (approximate).
558 fn max_columns(&self) -> usize {
559 10000
560 }
561
562 /// Maximum query parameters (approximate, 0 = unlimited).
563 fn max_parameters(&self) -> usize {
564 0
565 }
566
567 /// **The widest exact decimal a column on this platform holds**, where it
568 /// has been measured against the engine.
569 ///
570 /// A type over it is refused by name rather than rendered: the engine
571 /// would refuse the `CREATE TABLE` anyway, later and in its own words.
572 ///
573 /// The default is `None`, which means *not measured here*, not *no
574 /// limit*. An engine with no limit at all says so in its own doc comment
575 /// and still answers `None`.
576 fn decimal_limits(&self) -> Option<DecimalLimits> {
577 None
578 }
579
580 /// **What a text column on this platform holds**, where it has been
581 /// measured against the engine or read from its documentation (see
582 /// [`TextLimits`]).
583 ///
584 /// The default is `None`, which means *not recorded here*, not *no
585 /// limit*. Measure the longest `varchar(n)` your engine creates — one
586 /// more is refused at `CREATE TABLE` — and say how it counts it.
587 fn text_limits(&self) -> Option<TextLimits> {
588 None
589 }
590
591 /// **Whether this engine has a type that keeps a time zone**, for a
592 /// `timestamptz` column: a moment that keeps its offset, or one read in
593 /// the session's zone. The default is `true`. An engine with no such type
594 /// answers `false`, and a `timestamptz` column is refused by name rather
595 /// than stored as a clock time two hours off the moment the file named.
596 fn keeps_a_time_zone(&self) -> bool {
597 true
598 }
599
600 // ═══════════════════════════════════════════════════════════════
601 // Drop Capabilities
602 // ═══════════════════════════════════════════════════════════════
603
604 /// Whether DROP supports CASCADE modifier.
605 fn supports_cascade_drop(&self) -> bool {
606 true
607 }
608
609 /// Whether DROP FUNCTION requires parameter types for overloaded functions.
610 fn supports_function_overloading(&self) -> bool {
611 false
612 }
613
614 /// Whether DROP supports SYNC modifier (`ClickHouse`).
615 fn supports_sync_drop(&self) -> bool {
616 false
617 }
618
619 /// Whether DROP supports FORCE modifier (Databricks).
620 fn supports_force_drop(&self) -> bool {
621 false
622 }
623
624 // ═══════════════════════════════════════════════════════════════
625 // DML Extended Capabilities
626 // ═══════════════════════════════════════════════════════════════
627
628 /// Whether the platform supports compound assignment (e.g., `col += value`).
629 fn supports_compound_assignment(&self) -> bool {
630 false
631 }
632
633 /// Whether the platform supports EXCLUDE clause in window frame specs.
634 fn supports_window_frame_exclude(&self) -> bool {
635 false
636 }
637
638 /// Whether the platform supports MERGE NOT MATCHED BY SOURCE.
639 fn supports_merge_not_matched_by_source(&self) -> bool {
640 false
641 }
642
643 /// Keyword emitted for MERGE NOT MATCHED branch.
644 fn merge_not_matched_keyword(&self) -> &'static str {
645 "WHEN NOT MATCHED"
646 }
647
648 /// Whether the platform supports UPDATE SET * / INSERT * in MERGE.
649 fn supports_merge_star_syntax(&self) -> bool {
650 false
651 }
652
653 /// Whether an UPDATE is spelled `ALTER TABLE … UPDATE` (`ClickHouse`).
654 ///
655 /// **There is a second declaration of the same fact** on
656 /// [`crate::SqlRenderer::uses_alter_table_update`], and that one is the
657 /// declaration Razeri's generators consult. A dialect that overrides one
658 /// must override both and keep them agreeing; the duplication is named
659 /// here so a reader finds it before an ambiguous-method error does.
660 fn uses_alter_table_update(&self) -> bool {
661 false
662 }
663
664 /// Whether the platform supports multi-table INSERT (Snowflake).
665 fn supports_multi_table_insert(&self) -> bool {
666 false
667 }
668
669 // ═══════════════════════════════════════════════════════════════
670 // DDL / Namespace Capabilities
671 // ═══════════════════════════════════════════════════════════════
672
673 /// The SQL keyword for the schema-level namespace.
674 ///
675 /// Most platforms use `SCHEMA` (`CREATE SCHEMA IF NOT EXISTS ...`).
676 /// `ClickHouse` uses `DATABASE` (`CREATE DATABASE IF NOT EXISTS ...`).
677 fn schema_namespace_keyword(&self) -> &'static str {
678 "SCHEMA"
679 }
680
681 // ═══════════════════════════════════════════════════════════════
682 // Comment Capabilities
683 // ═══════════════════════════════════════════════════════════════
684
685 /// Whether the platform supports `#` as a single-line comment delimiter.
686 ///
687 /// Default: `false`. A dialect whose engine accepts `#` as a single-line
688 /// comment delimiter overrides this.
689 fn supports_hash_comments(&self) -> bool {
690 false
691 }
692
693 // ═══════════════════════════════════════════════════════════════
694 // Script Formatting
695 // ═══════════════════════════════════════════════════════════════
696
697 /// Batch separator keyword required between DDL statements in a script.
698 ///
699 /// SQL Server / T-SQL requires `GO` between `CREATE FUNCTION`, `CREATE VIEW`,
700 /// and `CREATE PROCEDURE` statements. Other platforms return `None` and use
701 /// double-newlines between statements.
702 fn batch_separator(&self) -> Option<&'static str> {
703 None
704 }
705
706 // ═══════════════════════════════════════════════════════════════
707 // Transaction Capabilities
708 // ═══════════════════════════════════════════════════════════════
709
710 /// Whether several statements in a script can be committed or discarded
711 /// together as one unit.
712 ///
713 /// A generated load is a sequence of writes that only make sense finished.
714 /// An SCD-2 load, for instance, closes the standing version into `_history`
715 /// and then re-opens `_current` on the new attributes; a process that dies
716 /// between the two leaves a closed version that `_current` still holds open,
717 /// and because `_history` is keyed on (surrogate, version start), the next
718 /// load writes the same key again and the engine refuses it. Every load
719 /// after the crash fails until somebody deletes that row by hand.
720 ///
721 /// Wrapping the load in one transaction is what makes the crash a no-op
722 /// instead. This flag says whether the platform offers that at all. It
723 /// defaults to `true` because a transaction spanning several statements is
724 /// core SQL and nearly every engine has it; a platform that does not
725 /// overrides it, and the load script then says plainly that the operator
726 /// owns the boundary.
727 fn supports_multi_statement_transactions(&self) -> bool {
728 true
729 }
730
731 /// How this platform opens a transaction.
732 ///
733 /// The ANSI spelling, which `MySQL` and `MariaDB` also use. `DuckDB`,
734 /// `SQLite`, `SQL Server` and `Snowflake` say `BEGIN TRANSACTION`;
735 /// `PostgreSQL` and `Citus` say `BEGIN`.
736 fn begin_transaction_keyword(&self) -> &'static str {
737 "START TRANSACTION"
738 }
739
740 /// How this platform commits one.
741 ///
742 /// `COMMIT` everywhere razeri reaches except `SQL Server`, which spells it
743 /// `COMMIT TRANSACTION`.
744 fn commit_transaction_keyword(&self) -> &'static str {
745 "COMMIT"
746 }
747
748 /// **Whether this engine's begin statement leaves the commit to the
749 /// client.**
750 ///
751 /// On most engines [`begin_transaction_keyword`](Self::begin_transaction_keyword)
752 /// opens a transaction that holds every statement after it until the
753 /// commit, whatever the client does. On some it only names the
754 /// transaction — Oracle's and SAP HANA's `SET TRANSACTION READ WRITE` —
755 /// and a driver that commits each statement on its own still does, so a
756 /// load run that way keeps what it wrote when it stops part way.
757 ///
758 /// Razeri's own deploy scripts drive clients whose commit is off. Answer
759 /// `true` where the engine needs that of the client, and the bundle tells
760 /// whoever runs a load another way — a scheduler running the task list —
761 /// to turn its client's commit off. The default is `false`.
762 fn begin_leaves_the_commit_to_the_client(&self) -> bool {
763 false
764 }
765
766 /// Whether emptying a table with `TRUNCATE` ends the open transaction.
767 ///
768 /// On some engines `TRUNCATE TABLE` commits everything before it and
769 /// cannot be rolled back: `MySQL` and `MariaDB`, which treat it as a
770 /// schema change, and Oracle, SAP HANA, `BigQuery` and Redshift. A
771 /// generated load empties its staging table only as its last statement,
772 /// after every write it makes, so on those engines the clear commits the
773 /// load with it and each load stands on its own; a load that stops before
774 /// it leaves nothing behind. Everywhere else `TRUNCATE` is an ordinary
775 /// statement inside the transaction.
776 fn truncate_commits_implicitly(&self) -> bool {
777 false
778 }
779
780 /// **Whether a load's refusal stops the statement that asks it.**
781 ///
782 /// A generated load stops itself by building a sentence about the rows it
783 /// refuses and asking the engine for
784 /// [`ExpressionRenderer::render_refusal`](crate::ExpressionRenderer::render_refusal)
785 /// of it: the engine refuses and quotes the sentence back in its error.
786 /// Most engines do. `MySQL` and `MariaDB` answer the default refusal, a
787 /// conversion to a number, with a warning and `0`, and Spark answers
788 /// `NULL` where ANSI mode is off. There the load stops on a second
789 /// statement instead, in the engine's own words and without the sentence,
790 /// so a deploy script reads the refused rows back and prints them itself.
791 ///
792 /// The default is `true`, the answer of every engine whose refusal stops.
793 fn refusal_stops_the_statement(&self) -> bool {
794 true
795 }
796
797 /// **Whether this engine's file reader may read a field holding just the
798 /// word `NULL` as a missing value** rather than as the four letters.
799 ///
800 /// A design names the words that mean "no value" in each file, and a
801 /// binding that names none calls `NULL` missing, which every engine then
802 /// agrees on. A file that keeps `NULL` as text cannot be read that way
803 /// here, so the bundle names the file, and a reconciliation does not
804 /// compare the characters of a text column holding the word. The default
805 /// is `false`: the engine keeps the text, as every engine's own file
806 /// reader does except `MySQL`'s and `MariaDB`'s `LOAD DATA`.
807 fn delimited_file_reads_null_word_as_missing(&self) -> bool {
808 false
809 }
810
811 // ═══════════════════════════════════════════════════════════════
812 // Storage / Partition / Compression Capabilities
813 // ═══════════════════════════════════════════════════════════════
814
815 /// Whether the platform supports a server-side partition function definable
816 /// at design time (`T-SQL` `PARTITION FUNCTION`, `PostgreSQL` `PARTITION BY`,
817 /// `MySQL` `PARTITION BY`, `ClickHouse` `PARTITION BY`).
818 ///
819 /// `Snowflake` / `DuckDB` return `false` because micro-partitioning is automatic
820 /// and not user-definable.
821 fn supports_partition_function(&self) -> bool {
822 false
823 }
824
825 /// Whether the platform supports adding/removing partition boundaries on a
826 /// live table (`T-SQL` `ALTER PARTITION FUNCTION SPLIT/MERGE RANGE`,
827 /// `PostgreSQL` `ATTACH/DETACH PARTITION`, `MySQL` `REORGANIZE PARTITION`).
828 fn supports_partition_split_merge(&self) -> bool {
829 false
830 }
831
832 /// Whether the platform supports a columnstore index (`T-SQL` clustered/non-clustered
833 /// columnstore; `ClickHouse` via `MergeTree` engine; not natively in `PostgreSQL` / `MySQL`).
834 fn supports_columnstore_index(&self) -> bool {
835 false
836 }
837
838 /// Whether the platform realizes a columnstore as a **table storage engine**
839 /// rather than an index (`MariaDB` `ENGINE=ColumnStore`). This is deliberately
840 /// distinct from [`Self::supports_columnstore_index`]: a design that asks for
841 /// `index: { kind: clustered_columnstore }` is honoured on such a platform by
842 /// selecting the engine, but it must NOT inherit the `T-SQL`
843 /// index machinery — the composite-PK demotion to `UNIQUE NONCLUSTERED` keys
844 /// off `supports_columnstore_index`, and an engine-columnstore keeps its
845 /// ordinary primary key. So the two capabilities are separate axes: "supports
846 /// columnstore at all" is the disjunction of them.
847 fn columnstore_is_table_engine(&self) -> bool {
848 false
849 }
850
851 /// Whether the platform has a clustering-key concept distinct from primary key
852 /// (`Snowflake` `CLUSTER BY`, Delta Liquid Clustering / `Z-ORDER`, `ClickHouse` `ORDER BY`).
853 fn supports_clustering_key(&self) -> bool {
854 false
855 }
856
857 /// Whether the platform supports user-controllable table compression
858 /// (`T-SQL` `DATA_COMPRESSION = PAGE/ROW/COLUMNSTORE`, `MySQL` `ROW_FORMAT=COMPRESSED`,
859 /// `ClickHouse` `CODEC(ZSTD)`, Delta `delta.compressionCodec`).
860 fn supports_table_compression(&self) -> bool {
861 false
862 }
863
864 /// Whether the platform requires every column referenced by the partition
865 /// key to also appear in the table's PRIMARY KEY (or any UNIQUE
866 /// constraint).
867 ///
868 /// `PostgreSQL` enforces this with `SQLSTATE 0A000` ("unique constraint on
869 /// partitioned table must include all partitioning columns"). `T-SQL`,
870 /// `MySQL`, `ClickHouse`, `Snowflake`, and `DuckDB` do not impose this
871 /// restriction.
872 fn partition_requires_pk_includes_field(&self) -> bool {
873 false
874 }
875}
876
877/// **How much of a deployed schema Razeri's reconciliation can read back from
878/// this engine's catalogue**, answered by
879/// [`DialectCapabilities::catalogue_reach`].
880#[derive(Debug, Clone, Copy, PartialEq, Eq)]
881#[non_exhaustive]
882pub enum CatalogueReach {
883 /// **Every check.** The engine runs the query that compares its catalogue
884 /// with the design in one measurement: `information_schema.columns`, a
885 /// `FULL OUTER JOIN`, and a table value constructor with a derived column
886 /// list.
887 Whole,
888 /// **The columns it holds, and not the comparison.** The engine has
889 /// `information_schema.columns` and lacks what the one-measurement
890 /// comparison needs, so the reconciliation asks only whether each designed
891 /// column is held. The sentence says what it lacks; a warning carries it.
892 HeldColumnsOnly(&'static str),
893 /// **No schema check.** The sentence says why, in a reader's words; a
894 /// warning carries it.
895 Nothing(&'static str),
896}
897
898#[cfg(test)]
899mod tests {
900 use super::*;
901 use crate::merge_shape::MergeRestrictions;
902
903 struct DefaultDialect;
904 impl DialectCapabilities for DefaultDialect {}
905
906 /// **A dialect that states no question asks none, and the first word of
907 /// an answer is its version.**
908 #[test]
909 fn the_server_version_is_the_first_word_of_the_answer() {
910 let d = DefaultDialect;
911 assert_eq!(d.server_version_query(), None);
912 let read = d.parse_server_version("17 17.2");
913 assert_eq!(read.map(|v| v.to_string()), Some("17".to_owned()));
914 assert_eq!(d.parse_server_version(" 8.4 MySQL Community Server"), "8.4".parse().ok());
915 assert_eq!(d.parse_server_version(""), None);
916 assert_eq!(d.parse_server_version("PostgreSQL 17.2"), None);
917 }
918
919 /// Assert default DML capability flags are conservative.
920 /// Parameters:
921 /// - d: Dialect capabilities implementation under test.
922 fn assert_default_dml_capabilities(d: &impl DialectCapabilities) {
923 assert_eq!(d.merge_shape(), MergeShape::None);
924 assert!(!d.supports_on_conflict());
925 assert!(!d.supports_insert_overwrite());
926 assert!(!d.supports_update_from());
927 assert!(!d.supports_returning());
928 }
929
930 /// Assert default query capability flags and ANSI-safe behavior.
931 /// Parameters:
932 /// - d: Dialect capabilities implementation under test.
933 fn assert_default_query_capabilities(d: &impl DialectCapabilities) {
934 assert!(d.supports_cte());
935 assert!(d.supports_recursive_cte());
936 assert!(d.supports_window_functions());
937 assert!(d.supports_named_windows());
938 assert!(!d.supports_qualify());
939 assert!(!d.supports_groups_frame());
940 assert!(d.supports_nulls_ordering());
941 }
942
943 /// Assert default DDL capability and type flags are conservative.
944 /// Parameters:
945 /// - d: Dialect capabilities implementation under test.
946 fn assert_default_ddl_and_type_capabilities(d: &impl DialectCapabilities) {
947 assert!(!d.supports_identity());
948 assert!(!d.supports_sequences());
949 assert!(d.supports_schemas());
950 assert!(!d.supports_bulk_load());
951 assert!(!d.supports_stored_procedures());
952 // The two routine-form questions default the other way on purpose:
953 // answering `false` REFUSES a build, so it is never the answer an
954 // unknown platform falls into.
955 assert!(d.supports_stored_functions());
956 assert!(d.supports_table_functions());
957 assert!(!d.supports_tasks());
958 assert!(d.supports_partitioning());
959 assert!(d.supports_materialized_views());
960 assert!(d.supports_generated_columns());
961 assert!(d.supports_json());
962 assert!(!d.supports_unsigned_integers());
963 assert!(!d.supports_arrays());
964 assert!(d.supports_boolean());
965 }
966
967 /// Assert default platform limits and storage capability flags.
968 /// Parameters:
969 /// - d: Dialect capabilities implementation under test.
970 fn assert_default_limits_and_storage_capabilities(d: &impl DialectCapabilities) {
971 assert_eq!(d.default_schema(), "public");
972 assert_eq!(d.max_identifier_length(), 63);
973 assert_eq!(d.max_columns(), 10000);
974 assert_eq!(d.max_parameters(), 0);
975 assert!(!d.supports_partition_function());
976 assert!(!d.supports_partition_split_merge());
977 assert!(!d.supports_columnstore_index());
978 assert!(!d.supports_clustering_key());
979 assert!(!d.supports_table_compression());
980 }
981
982 #[test]
983 fn defaults_are_conservative() {
984 let d = DefaultDialect;
985 assert_default_dml_capabilities(&d);
986 assert_default_query_capabilities(&d);
987 assert_default_ddl_and_type_capabilities(&d);
988 assert_default_limits_and_storage_capabilities(&d);
989 }
990
991 #[test]
992 fn overrides_work() {
993 struct PostgresLike;
994 impl DialectCapabilities for PostgresLike {
995 fn merge_shape(&self) -> MergeShape {
996 MergeShape::Merge(MergeRestrictions::none())
997 }
998 fn supports_on_conflict(&self) -> bool {
999 true
1000 }
1001 fn supports_returning(&self) -> bool {
1002 true
1003 }
1004 fn supports_identity(&self) -> bool {
1005 true
1006 }
1007 fn supports_sequences(&self) -> bool {
1008 true
1009 }
1010 fn supports_arrays(&self) -> bool {
1011 true
1012 }
1013 fn max_columns(&self) -> usize {
1014 1600
1015 }
1016 }
1017
1018 let d = PostgresLike;
1019 assert!(d.merge_shape().merges());
1020 assert!(d.supports_on_conflict());
1021 assert!(d.supports_returning());
1022 assert!(d.supports_identity());
1023 assert!(d.supports_sequences());
1024 assert!(d.supports_arrays());
1025 assert_eq!(d.max_columns(), 1600);
1026 // Untouched defaults
1027 assert!(!d.supports_insert_overwrite());
1028 assert!(d.supports_cte());
1029 }
1030
1031 #[test]
1032 fn new_capability_defaults_are_conservative() {
1033 let d = DefaultDialect;
1034 // Drop capabilities
1035 assert!(d.supports_cascade_drop()); // true = ANSI default
1036 assert!(!d.supports_function_overloading());
1037 assert!(!d.supports_sync_drop());
1038 assert!(!d.supports_force_drop());
1039 // DML extended
1040 assert!(!d.supports_compound_assignment());
1041 assert!(!d.supports_window_frame_exclude());
1042 assert!(!d.supports_merge_not_matched_by_source());
1043 assert_eq!(d.merge_not_matched_keyword(), "WHEN NOT MATCHED");
1044 assert!(!d.supports_merge_star_syntax());
1045 assert!(!d.uses_alter_table_update());
1046 assert!(!d.supports_multi_table_insert());
1047 // DDL / Namespace
1048 assert_eq!(d.schema_namespace_keyword(), "SCHEMA");
1049 }
1050
1051 /// The default transaction answer is the ANSI one: a load can be wrapped,
1052 /// in the standard spelling, and clearing a staging table stays inside the
1053 /// wrap. Every platform that differs says so on its own dialect.
1054 #[test]
1055 fn the_default_load_wrap_is_the_ansi_one() {
1056 let d = DefaultDialect;
1057 assert!(d.supports_multi_statement_transactions());
1058 assert_eq!(d.begin_transaction_keyword(), "START TRANSACTION");
1059 assert_eq!(d.commit_transaction_keyword(), "COMMIT");
1060 assert!(!d.truncate_commits_implicitly());
1061 assert!(!d.begin_leaves_the_commit_to_the_client());
1062 }
1063
1064 /// **A bound on the digits before the point is its own refusal**, and the
1065 /// nearest type that fits keeps the scale and gives up whole digits.
1066 #[test]
1067 fn digits_before_the_point_have_their_own_bound() {
1068 let bignumeric = DecimalLimits {
1069 max_precision: 76,
1070 max_scale: 38,
1071 scale_within_precision: true,
1072 max_integer_digits: Some(38),
1073 };
1074 assert_eq!(bignumeric.misfit(38, 0), None);
1075 assert_eq!(bignumeric.misfit(76, 38), None);
1076 assert_eq!(
1077 bignumeric.misfit(39, 0).as_deref(),
1078 Some("its decimals hold at most 38 digits before the point, and this asks for 39")
1079 );
1080 assert_eq!(bignumeric.nearest(39, 0), (38, 0));
1081 assert_eq!(bignumeric.nearest(60, 10), (48, 10));
1082 assert_eq!(bignumeric.nearest(99, 40), (76, 38));
1083 for (precision, scale) in [(39, 0), (60, 10), (99, 40), (5, 6), (0, 0)] {
1084 let (p, s) = bignumeric.nearest(precision, scale);
1085 assert_eq!(bignumeric.misfit(p, s), None, "nearest({precision}, {scale}) fits");
1086 }
1087
1088 let unbounded = DecimalLimits { max_integer_digits: None, ..bignumeric };
1089 assert_eq!(unbounded.misfit(76, 0), None, "the digits in all are the only bound");
1090 }
1091}