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