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