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