Skip to main content

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 a generated load is written as a stored procedure** on this
268    /// engine.
269    ///
270    /// Razeri asks this before it writes a load as a procedure, and asks
271    /// [`SqlRenderer::procedure_form`](crate::SqlRenderer::procedure_form)
272    /// how to spell one. The default is `false`, and a load is then written as
273    /// plain statements. A dialect that answers `procedure_form` with
274    /// `ProcedureForm::Custom` answers `true` here, or its procedures are
275    /// never asked for; one that answers `ProcedureForm::Unsupported` leaves
276    /// this `false`. The conformance report holds the two to each other.
277    fn supports_stored_procedures(&self) -> bool {
278        false
279    }
280
281    /// Whether the platform **stores functions at all** — a routine the schema
282    /// creates and later calls by name, in any spelling.
283    ///
284    /// Nearly every engine razeri reaches has one: `CREATE FUNCTION` on
285    /// `PostgreSQL`, `MySQL`, `MariaDB`, Snowflake, `SQL Server` and Databricks,
286    /// a `MACRO` on `DuckDB`, a lambda `CREATE FUNCTION` on `ClickHouse`.
287    ///
288    /// **`SQLite` answers `false`, and it is the reason this question is asked
289    /// out loud.** `SQLite` has no `CREATE FUNCTION` statement in any spelling
290    /// and no `CREATE PROCEDURE` either — a function there is registered by the
291    /// program that opens the database, so nothing written into a schema script
292    /// can create one. Inheriting the `PostgreSQL` form wrote
293    /// `CREATE OR REPLACE FUNCTION … LANGUAGE SQL RETURN …` into `schema.sql`,
294    /// which `SQLite` answers with `near "CREATE": syntax error` part way
295    /// through the script, so the build refuses in front of the script instead.
296    ///
297    /// The default is `true`, matching [`Self::supports_table_functions`] and
298    /// for the same reason: refusing is the destructive answer, so a refusal
299    /// fires only where an engine is KNOWN to have no form, never because a
300    /// dialect forgot to answer. That reverses the conservative-`false` default
301    /// this trait uses for flags that *add* syntax — here `false` is the
302    /// unusual claim, and it is made deliberately, one dialect at a time.
303    fn supports_stored_functions(&self) -> bool {
304        true
305    }
306
307    /// Whether the platform has a **table-valued function** — a routine a query
308    /// selects FROM, returning columns rather than one value.
309    ///
310    /// Most engines have one in some spelling: `RETURNS TABLE(…)` on
311    /// `PostgreSQL`, Snowflake and Databricks, an inline TVF on SQL Server, a
312    /// table `MACRO` on `DuckDB`, a parameterized view on `ClickHouse`.
313    ///
314    /// **`MySQL` and `MariaDB` answer `false`, and they are the reason this
315    /// question exists.** A stored function there returns exactly one value;
316    /// there is no clause that returns a relation, in any spelling. Rendering
317    /// `RETURNS TABLE` for them produced `ERROR 1064 … syntax error` in the
318    /// middle of a schema script (measured on `MySQL` 8.4 and `MariaDB` 11.4),
319    /// so the build refuses in front of the script instead.
320    ///
321    /// The default is `true`: a refusal fires only where an engine is KNOWN to
322    /// have no form, never because a dialect forgot to answer.
323    fn supports_table_functions(&self) -> bool {
324        true
325    }
326
327    /// Whether the platform supports tasks / scheduled jobs.
328    fn supports_tasks(&self) -> bool {
329        false
330    }
331
332    /// Whether the platform supports native table partitioning.
333    fn supports_partitioning(&self) -> bool {
334        true
335    }
336
337    /// Whether the platform supports materialized views.
338    fn supports_materialized_views(&self) -> bool {
339        true
340    }
341
342    /// Whether the platform supports generated/computed columns.
343    fn supports_generated_columns(&self) -> bool {
344        true
345    }
346
347    /// Whether the platform supports JSONB or native JSON type.
348    fn supports_json(&self) -> bool {
349        true
350    }
351
352    // ═══════════════════════════════════════════════════════════════
353    // Type Capabilities
354    // ═══════════════════════════════════════════════════════════════
355
356    /// Whether the platform supports unsigned integers natively.
357    fn supports_unsigned_integers(&self) -> bool {
358        false
359    }
360
361    /// Whether the platform supports arrays natively.
362    fn supports_arrays(&self) -> bool {
363        false
364    }
365
366    /// **Whether this engine has a type of its own for a nested type, or
367    /// stores it as something else.**
368    ///
369    /// A nested column is an array, a map or a record. Some engines have all
370    /// three; some have one; some have none and hold the value as JSON or as
371    /// text. Razeri never refuses the column — the design is portable and it is
372    /// the storage that degrades — but it does say so once, naming the engine,
373    /// the columns and what each became.
374    ///
375    /// The default is `false`, which is the honest answer for an engine nobody
376    /// has answered for: the reader is told what the column became rather than
377    /// left to assume the type survived.
378    ///
379    /// Parameters:
380    /// - `data_type`: the nested type in question.
381    fn renders_nested_type_natively(&self, data_type: &DataType) -> bool {
382        let _ = data_type;
383        false
384    }
385
386    /// **Whether this engine can tell two values of a nested type apart.**
387    ///
388    /// Change detection asks whether an arriving value differs from the
389    /// standing one. For a scalar every engine answers with `<>`. For a nested
390    /// value they do not agree: some compare arrays, maps and records directly,
391    /// some refuse the comparison outright, and some hold the value as text and
392    /// compare the text. A column this returns `false` for is left out of
393    /// change detection and named in a note, rather than compared with an
394    /// operator the engine will reject.
395    ///
396    /// The default is `false` — an unanswered engine leaves the column
397    /// unwatched and says so, which is the failure that can be read.
398    ///
399    /// Parameters:
400    /// - `data_type`: the nested type in question.
401    fn compares_nested_values(&self, data_type: &DataType) -> bool {
402        let _ = data_type;
403        false
404    }
405
406    /// **Whether this engine can collapse rows on a nested column** — group by
407    /// it, or select distinct over it.
408    ///
409    /// A load reads its staged rows through one collapsed form so that a row
410    /// stated twice, identically, is one arriving row rather than a conflict.
411    /// The plain spelling of that collapse is `SELECT DISTINCT` over every
412    /// column, and grouping is what `DISTINCT` is: an engine that will not
413    /// group a value of a nested type will not take the column in a `DISTINCT`
414    /// either.
415    ///
416    /// It is a separate question from [`Self::compares_nested_values`], and the
417    /// two engines that answer them differently are the reason it is asked
418    /// separately: `BigQuery` compares two records with `=` and refuses to group
419    /// on one.
420    ///
421    /// Where the answer is `false` the collapse is written as a `GROUP BY` over
422    /// the other columns with [`crate::SqlRenderer::render_any_value`] around
423    /// this one.
424    ///
425    /// The default is `true`: `SELECT DISTINCT` over every column is the plain
426    /// spelling, and an engine says otherwise only where somebody has found
427    /// that it refuses.
428    ///
429    /// Asked only of a nested type — an array, a map or a record. The answer
430    /// for anything else is not read.
431    ///
432    /// Parameters:
433    /// - `data_type`: the nested type in question.
434    fn groups_nested_values(&self, data_type: &DataType) -> bool {
435        let _ = data_type;
436        true
437    }
438
439    /// Whether the platform supports native BOOLEAN type (vs BIT).
440    fn supports_boolean(&self) -> bool {
441        true
442    }
443
444    // ═══════════════════════════════════════════════════════════════
445    // Limits & Defaults
446    // ═══════════════════════════════════════════════════════════════
447
448    /// The default schema name for this platform.
449    fn default_schema(&self) -> &'static str {
450        "public"
451    }
452
453    /// Maximum identifier length for this platform.
454    fn max_identifier_length(&self) -> usize {
455        63
456    }
457
458    /// Maximum number of columns in a table (approximate).
459    fn max_columns(&self) -> usize {
460        10000
461    }
462
463    /// Maximum query parameters (approximate, 0 = unlimited).
464    fn max_parameters(&self) -> usize {
465        0
466    }
467
468    /// **The widest exact decimal a column on this platform holds**, where it
469    /// has been measured against the engine.
470    ///
471    /// A type over it is refused by name rather than rendered: the engine
472    /// would refuse the `CREATE TABLE` anyway, later and in its own words.
473    ///
474    /// The default is `None`, which means *not measured here*, not *no
475    /// limit*. An engine with no limit at all says so in its own doc comment
476    /// and still answers `None`.
477    fn decimal_limits(&self) -> Option<DecimalLimits> {
478        None
479    }
480
481    // ═══════════════════════════════════════════════════════════════
482    // Drop Capabilities
483    // ═══════════════════════════════════════════════════════════════
484
485    /// Whether DROP supports CASCADE modifier.
486    fn supports_cascade_drop(&self) -> bool {
487        true
488    }
489
490    /// Whether DROP FUNCTION requires parameter types for overloaded functions.
491    fn supports_function_overloading(&self) -> bool {
492        false
493    }
494
495    /// Whether DROP supports SYNC modifier (`ClickHouse`).
496    fn supports_sync_drop(&self) -> bool {
497        false
498    }
499
500    /// Whether DROP supports FORCE modifier (Databricks).
501    fn supports_force_drop(&self) -> bool {
502        false
503    }
504
505    // ═══════════════════════════════════════════════════════════════
506    // DML Extended Capabilities
507    // ═══════════════════════════════════════════════════════════════
508
509    /// Whether the platform supports compound assignment (e.g., `col += value`).
510    fn supports_compound_assignment(&self) -> bool {
511        false
512    }
513
514    /// Whether the platform supports EXCLUDE clause in window frame specs.
515    fn supports_window_frame_exclude(&self) -> bool {
516        false
517    }
518
519    /// Whether the platform supports MERGE NOT MATCHED BY SOURCE.
520    fn supports_merge_not_matched_by_source(&self) -> bool {
521        false
522    }
523
524    /// Keyword emitted for MERGE NOT MATCHED branch.
525    fn merge_not_matched_keyword(&self) -> &'static str {
526        "WHEN NOT MATCHED"
527    }
528
529    /// Whether the platform supports UPDATE SET * / INSERT * in MERGE.
530    fn supports_merge_star_syntax(&self) -> bool {
531        false
532    }
533
534    /// Whether an UPDATE is spelled `ALTER TABLE … UPDATE` (`ClickHouse`).
535    ///
536    /// **There is a second declaration of the same fact** on
537    /// [`crate::SqlRenderer::uses_alter_table_update`], and that one is the
538    /// declaration Razeri's generators consult. A dialect that overrides one
539    /// must override both and keep them agreeing; the duplication is named
540    /// here so a reader finds it before an ambiguous-method error does.
541    fn uses_alter_table_update(&self) -> bool {
542        false
543    }
544
545    /// Whether the platform supports multi-table INSERT (Snowflake).
546    fn supports_multi_table_insert(&self) -> bool {
547        false
548    }
549
550    // ═══════════════════════════════════════════════════════════════
551    // DDL / Namespace Capabilities
552    // ═══════════════════════════════════════════════════════════════
553
554    /// The SQL keyword for the schema-level namespace.
555    ///
556    /// Most platforms use `SCHEMA` (`CREATE SCHEMA IF NOT EXISTS ...`).
557    /// `ClickHouse` uses `DATABASE` (`CREATE DATABASE IF NOT EXISTS ...`).
558    fn schema_namespace_keyword(&self) -> &'static str {
559        "SCHEMA"
560    }
561
562    // ═══════════════════════════════════════════════════════════════
563    // Comment Capabilities
564    // ═══════════════════════════════════════════════════════════════
565
566    /// Whether the platform supports `#` as a single-line comment delimiter.
567    ///
568    /// Default: `false`. A dialect whose engine accepts `#` as a single-line
569    /// comment delimiter overrides this.
570    fn supports_hash_comments(&self) -> bool {
571        false
572    }
573
574    // ═══════════════════════════════════════════════════════════════
575    // Script Formatting
576    // ═══════════════════════════════════════════════════════════════
577
578    /// Batch separator keyword required between DDL statements in a script.
579    ///
580    /// SQL Server / T-SQL requires `GO` between `CREATE FUNCTION`, `CREATE VIEW`,
581    /// and `CREATE PROCEDURE` statements. Other platforms return `None` and use
582    /// double-newlines between statements.
583    fn batch_separator(&self) -> Option<&'static str> {
584        None
585    }
586
587    // ═══════════════════════════════════════════════════════════════
588    // Transaction Capabilities
589    // ═══════════════════════════════════════════════════════════════
590
591    /// Whether several statements in a script can be committed or discarded
592    /// together as one unit.
593    ///
594    /// A generated load is a sequence of writes that only make sense finished.
595    /// An SCD-2 load, for instance, closes the standing version into `_history`
596    /// and then re-opens `_current` on the new attributes; a process that dies
597    /// between the two leaves a closed version that `_current` still holds open,
598    /// and because `_history` is keyed on (surrogate, version start), the next
599    /// load writes the same key again and the engine refuses it. Every load
600    /// after the crash fails until somebody deletes that row by hand.
601    ///
602    /// Wrapping the load in one transaction is what makes the crash a no-op
603    /// instead. This flag says whether the platform offers that at all. It
604    /// defaults to `true` because a transaction spanning several statements is
605    /// core SQL and nearly every engine has it; a platform that does not
606    /// overrides it, and the load script then says plainly that the operator
607    /// owns the boundary.
608    fn supports_multi_statement_transactions(&self) -> bool {
609        true
610    }
611
612    /// How this platform opens a transaction.
613    ///
614    /// The ANSI spelling, which `MySQL` and `MariaDB` also use. `DuckDB`,
615    /// `SQLite`, `SQL Server` and `Snowflake` say `BEGIN TRANSACTION`;
616    /// `PostgreSQL` and `Citus` say `BEGIN`.
617    fn begin_transaction_keyword(&self) -> &'static str {
618        "START TRANSACTION"
619    }
620
621    /// How this platform commits one.
622    ///
623    /// `COMMIT` everywhere razeri reaches except `SQL Server`, which spells it
624    /// `COMMIT TRANSACTION`.
625    fn commit_transaction_keyword(&self) -> &'static str {
626        "COMMIT"
627    }
628
629    /// Whether emptying a table with `TRUNCATE` ends the open transaction.
630    ///
631    /// `MySQL` and `MariaDB` treat `TRUNCATE TABLE` as schema change, and a
632    /// schema change commits everything before it and cannot be rolled back.
633    /// A generated load clears its staging table that way, so on those two
634    /// platforms the transaction around the load does not reach back past the
635    /// most recent staging clear. Everywhere else `TRUNCATE` is an ordinary
636    /// statement inside the transaction.
637    fn truncate_commits_implicitly(&self) -> bool {
638        false
639    }
640
641    /// **Whether a load's refusal stops the statement that asks it.**
642    ///
643    /// A generated load stops itself by building a sentence about the rows it
644    /// refuses and asking the engine for
645    /// [`ExpressionRenderer::render_refusal`](crate::ExpressionRenderer::render_refusal)
646    /// of it: the engine refuses and quotes the sentence back in its error.
647    /// Most engines do. `MySQL` and `MariaDB` answer the default refusal, a
648    /// conversion to a number, with a warning and `0`, and Spark answers
649    /// `NULL` where ANSI mode is off. There the load stops on a second
650    /// statement instead, in the engine's own words and without the sentence,
651    /// so a deploy script reads the refused rows back and prints them itself.
652    ///
653    /// The default is `true`, the answer of every engine whose refusal stops.
654    fn refusal_stops_the_statement(&self) -> bool {
655        true
656    }
657
658    /// **Whether this engine's file reader may read a field holding just the
659    /// word `NULL` as a missing value** rather than as the four letters.
660    ///
661    /// A design names the words that mean "no value" in each file, and a
662    /// binding that names none calls `NULL` missing, which every engine then
663    /// agrees on. A file that keeps `NULL` as text cannot be read that way
664    /// here, so the bundle names the file, and a reconciliation does not
665    /// compare the characters of a text column holding the word. The default
666    /// is `false`: the engine keeps the text, as every engine's own file
667    /// reader does except `MySQL`'s and `MariaDB`'s `LOAD DATA`.
668    fn delimited_file_reads_null_word_as_missing(&self) -> bool {
669        false
670    }
671
672    // ═══════════════════════════════════════════════════════════════
673    // Storage / Partition / Compression Capabilities
674    // ═══════════════════════════════════════════════════════════════
675
676    /// Whether the platform supports a server-side partition function definable
677    /// at design time (`T-SQL` `PARTITION FUNCTION`, `PostgreSQL` `PARTITION BY`,
678    /// `MySQL` `PARTITION BY`, `ClickHouse` `PARTITION BY`).
679    ///
680    /// `Snowflake` / `DuckDB` return `false` because micro-partitioning is automatic
681    /// and not user-definable.
682    fn supports_partition_function(&self) -> bool {
683        false
684    }
685
686    /// Whether the platform supports adding/removing partition boundaries on a
687    /// live table (`T-SQL` `ALTER PARTITION FUNCTION SPLIT/MERGE RANGE`,
688    /// `PostgreSQL` `ATTACH/DETACH PARTITION`, `MySQL` `REORGANIZE PARTITION`).
689    fn supports_partition_split_merge(&self) -> bool {
690        false
691    }
692
693    /// Whether the platform supports a columnstore index (`T-SQL` clustered/non-clustered
694    /// columnstore; `ClickHouse` via `MergeTree` engine; not natively in `PostgreSQL` / `MySQL`).
695    fn supports_columnstore_index(&self) -> bool {
696        false
697    }
698
699    /// Whether the platform realizes a columnstore as a **table storage engine**
700    /// rather than an index (`MariaDB` `ENGINE=ColumnStore`). This is deliberately
701    /// distinct from [`Self::supports_columnstore_index`]: a design that asks for
702    /// `index: { kind: clustered_columnstore }` is honoured on such a platform by
703    /// selecting the engine, but it must NOT inherit the `T-SQL`
704    /// index machinery — the composite-PK demotion to `UNIQUE NONCLUSTERED` keys
705    /// off `supports_columnstore_index`, and an engine-columnstore keeps its
706    /// ordinary primary key. So the two capabilities are separate axes: "supports
707    /// columnstore at all" is the disjunction of them.
708    fn columnstore_is_table_engine(&self) -> bool {
709        false
710    }
711
712    /// Whether the platform has a clustering-key concept distinct from primary key
713    /// (`Snowflake` `CLUSTER BY`, Delta Liquid Clustering / `Z-ORDER`, `ClickHouse` `ORDER BY`).
714    fn supports_clustering_key(&self) -> bool {
715        false
716    }
717
718    /// Whether the platform supports user-controllable table compression
719    /// (`T-SQL` `DATA_COMPRESSION = PAGE/ROW/COLUMNSTORE`, `MySQL` `ROW_FORMAT=COMPRESSED`,
720    /// `ClickHouse` `CODEC(ZSTD)`, Delta `delta.compressionCodec`).
721    fn supports_table_compression(&self) -> bool {
722        false
723    }
724
725    /// Whether the platform requires every column referenced by the partition
726    /// key to also appear in the table's PRIMARY KEY (or any UNIQUE
727    /// constraint).
728    ///
729    /// `PostgreSQL` enforces this with `SQLSTATE 0A000` ("unique constraint on
730    /// partitioned table must include all partitioning columns"). `T-SQL`,
731    /// `MySQL`, `ClickHouse`, `Snowflake`, and `DuckDB` do not impose this
732    /// restriction.
733    fn partition_requires_pk_includes_field(&self) -> bool {
734        false
735    }
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741    use crate::merge_shape::MergeRestrictions;
742
743    struct DefaultDialect;
744    impl DialectCapabilities for DefaultDialect {}
745
746    /// **A dialect that states no question asks none, and the first word of
747    /// an answer is its version.**
748    #[test]
749    fn the_server_version_is_the_first_word_of_the_answer() {
750        let d = DefaultDialect;
751        assert_eq!(d.server_version_query(), None);
752        let read = d.parse_server_version("17 17.2");
753        assert_eq!(read.map(|v| v.to_string()), Some("17".to_owned()));
754        assert_eq!(d.parse_server_version("  8.4 MySQL Community Server"), "8.4".parse().ok());
755        assert_eq!(d.parse_server_version(""), None);
756        assert_eq!(d.parse_server_version("PostgreSQL 17.2"), None);
757    }
758
759    /// Assert default DML capability flags are conservative.
760    /// Parameters:
761    /// - d: Dialect capabilities implementation under test.
762    fn assert_default_dml_capabilities(d: &impl DialectCapabilities) {
763        assert_eq!(d.merge_shape(), MergeShape::None);
764        assert!(!d.supports_on_conflict());
765        assert!(!d.supports_insert_overwrite());
766        assert!(!d.supports_update_from());
767        assert!(!d.supports_returning());
768    }
769
770    /// Assert default query capability flags and ANSI-safe behavior.
771    /// Parameters:
772    /// - d: Dialect capabilities implementation under test.
773    fn assert_default_query_capabilities(d: &impl DialectCapabilities) {
774        assert!(d.supports_cte());
775        assert!(d.supports_recursive_cte());
776        assert!(d.supports_window_functions());
777        assert!(d.supports_named_windows());
778        assert!(!d.supports_qualify());
779        assert!(!d.supports_groups_frame());
780        assert!(d.supports_nulls_ordering());
781    }
782
783    /// Assert default DDL capability and type flags are conservative.
784    /// Parameters:
785    /// - d: Dialect capabilities implementation under test.
786    fn assert_default_ddl_and_type_capabilities(d: &impl DialectCapabilities) {
787        assert!(!d.supports_identity());
788        assert!(!d.supports_sequences());
789        assert!(d.supports_schemas());
790        assert!(!d.supports_bulk_load());
791        assert!(!d.supports_stored_procedures());
792        // The two routine-form questions default the other way on purpose:
793        // answering `false` REFUSES a build, so it is never the answer an
794        // unknown platform falls into.
795        assert!(d.supports_stored_functions());
796        assert!(d.supports_table_functions());
797        assert!(!d.supports_tasks());
798        assert!(d.supports_partitioning());
799        assert!(d.supports_materialized_views());
800        assert!(d.supports_generated_columns());
801        assert!(d.supports_json());
802        assert!(!d.supports_unsigned_integers());
803        assert!(!d.supports_arrays());
804        assert!(d.supports_boolean());
805    }
806
807    /// Assert default platform limits and storage capability flags.
808    /// Parameters:
809    /// - d: Dialect capabilities implementation under test.
810    fn assert_default_limits_and_storage_capabilities(d: &impl DialectCapabilities) {
811        assert_eq!(d.default_schema(), "public");
812        assert_eq!(d.max_identifier_length(), 63);
813        assert_eq!(d.max_columns(), 10000);
814        assert_eq!(d.max_parameters(), 0);
815        assert!(!d.supports_partition_function());
816        assert!(!d.supports_partition_split_merge());
817        assert!(!d.supports_columnstore_index());
818        assert!(!d.supports_clustering_key());
819        assert!(!d.supports_table_compression());
820    }
821
822    #[test]
823    fn defaults_are_conservative() {
824        let d = DefaultDialect;
825        assert_default_dml_capabilities(&d);
826        assert_default_query_capabilities(&d);
827        assert_default_ddl_and_type_capabilities(&d);
828        assert_default_limits_and_storage_capabilities(&d);
829    }
830
831    #[test]
832    fn overrides_work() {
833        struct PostgresLike;
834        impl DialectCapabilities for PostgresLike {
835            fn merge_shape(&self) -> MergeShape {
836                MergeShape::Merge(MergeRestrictions::none())
837            }
838            fn supports_on_conflict(&self) -> bool {
839                true
840            }
841            fn supports_returning(&self) -> bool {
842                true
843            }
844            fn supports_identity(&self) -> bool {
845                true
846            }
847            fn supports_sequences(&self) -> bool {
848                true
849            }
850            fn supports_arrays(&self) -> bool {
851                true
852            }
853            fn max_columns(&self) -> usize {
854                1600
855            }
856        }
857
858        let d = PostgresLike;
859        assert!(d.merge_shape().merges());
860        assert!(d.supports_on_conflict());
861        assert!(d.supports_returning());
862        assert!(d.supports_identity());
863        assert!(d.supports_sequences());
864        assert!(d.supports_arrays());
865        assert_eq!(d.max_columns(), 1600);
866        // Untouched defaults
867        assert!(!d.supports_insert_overwrite());
868        assert!(d.supports_cte());
869    }
870
871    #[test]
872    fn new_capability_defaults_are_conservative() {
873        let d = DefaultDialect;
874        // Drop capabilities
875        assert!(d.supports_cascade_drop()); // true = ANSI default
876        assert!(!d.supports_function_overloading());
877        assert!(!d.supports_sync_drop());
878        assert!(!d.supports_force_drop());
879        // DML extended
880        assert!(!d.supports_compound_assignment());
881        assert!(!d.supports_window_frame_exclude());
882        assert!(!d.supports_merge_not_matched_by_source());
883        assert_eq!(d.merge_not_matched_keyword(), "WHEN NOT MATCHED");
884        assert!(!d.supports_merge_star_syntax());
885        assert!(!d.uses_alter_table_update());
886        assert!(!d.supports_multi_table_insert());
887        // DDL / Namespace
888        assert_eq!(d.schema_namespace_keyword(), "SCHEMA");
889    }
890
891    /// The default transaction answer is the ANSI one: a load can be wrapped,
892    /// in the standard spelling, and clearing a staging table stays inside the
893    /// wrap. Every platform that differs says so on its own dialect.
894    #[test]
895    fn the_default_load_wrap_is_the_ansi_one() {
896        let d = DefaultDialect;
897        assert!(d.supports_multi_statement_transactions());
898        assert_eq!(d.begin_transaction_keyword(), "START TRANSACTION");
899        assert_eq!(d.commit_transaction_keyword(), "COMMIT");
900        assert!(!d.truncate_commits_implicitly());
901    }
902
903    /// **A bound on the digits before the point is its own refusal**, and the
904    /// nearest type that fits keeps the scale and gives up whole digits.
905    #[test]
906    fn digits_before_the_point_have_their_own_bound() {
907        let bignumeric = DecimalLimits {
908            max_precision: 76,
909            max_scale: 38,
910            scale_within_precision: true,
911            max_integer_digits: Some(38),
912        };
913        assert_eq!(bignumeric.misfit(38, 0), None);
914        assert_eq!(bignumeric.misfit(76, 38), None);
915        assert_eq!(
916            bignumeric.misfit(39, 0).as_deref(),
917            Some("its decimals hold at most 38 digits before the point, and this asks for 39")
918        );
919        assert_eq!(bignumeric.nearest(39, 0), (38, 0));
920        assert_eq!(bignumeric.nearest(60, 10), (48, 10));
921        assert_eq!(bignumeric.nearest(99, 40), (76, 38));
922        for (precision, scale) in [(39, 0), (60, 10), (99, 40), (5, 6), (0, 0)] {
923            let (p, s) = bignumeric.nearest(precision, scale);
924            assert_eq!(bignumeric.misfit(p, s), None, "nearest({precision}, {scale}) fits");
925        }
926
927        let unbounded = DecimalLimits { max_integer_digits: None, ..bignumeric };
928        assert_eq!(unbounded.misfit(76, 0), None, "the digits in all are the only bound");
929    }
930}