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 // ═══════════════════════════════════════════════════════════════
602 // Storage / Partition / Compression Capabilities
603 // ═══════════════════════════════════════════════════════════════
604
605 /// Whether the platform supports a server-side partition function definable
606 /// at design time (`T-SQL` `PARTITION FUNCTION`, `PostgreSQL` `PARTITION BY`,
607 /// `MySQL` `PARTITION BY`, `ClickHouse` `PARTITION BY`).
608 ///
609 /// `Snowflake` / `DuckDB` return `false` because micro-partitioning is automatic
610 /// and not user-definable.
611 fn supports_partition_function(&self) -> bool {
612 false
613 }
614
615 /// Whether the platform supports adding/removing partition boundaries on a
616 /// live table (`T-SQL` `ALTER PARTITION FUNCTION SPLIT/MERGE RANGE`,
617 /// `PostgreSQL` `ATTACH/DETACH PARTITION`, `MySQL` `REORGANIZE PARTITION`).
618 fn supports_partition_split_merge(&self) -> bool {
619 false
620 }
621
622 /// Whether the platform supports a columnstore index (`T-SQL` clustered/non-clustered
623 /// columnstore; `ClickHouse` via `MergeTree` engine; not natively in `PostgreSQL` / `MySQL`).
624 fn supports_columnstore_index(&self) -> bool {
625 false
626 }
627
628 /// Whether the platform realizes a columnstore as a **table storage engine**
629 /// rather than an index (`MariaDB` `ENGINE=ColumnStore`). This is deliberately
630 /// distinct from [`Self::supports_columnstore_index`]: a design that asks for
631 /// `index: { kind: clustered_columnstore }` is honoured on such a platform by
632 /// selecting the engine, but it must NOT inherit the `T-SQL`
633 /// index machinery — the composite-PK demotion to `UNIQUE NONCLUSTERED` keys
634 /// off `supports_columnstore_index`, and an engine-columnstore keeps its
635 /// ordinary primary key. So the two capabilities are separate axes: "supports
636 /// columnstore at all" is the disjunction of them.
637 fn columnstore_is_table_engine(&self) -> bool {
638 false
639 }
640
641 /// Whether the platform has a clustering-key concept distinct from primary key
642 /// (`Snowflake` `CLUSTER BY`, Delta Liquid Clustering / `Z-ORDER`, `ClickHouse` `ORDER BY`).
643 fn supports_clustering_key(&self) -> bool {
644 false
645 }
646
647 /// Whether the platform supports user-controllable table compression
648 /// (`T-SQL` `DATA_COMPRESSION = PAGE/ROW/COLUMNSTORE`, `MySQL` `ROW_FORMAT=COMPRESSED`,
649 /// `ClickHouse` `CODEC(ZSTD)`, Delta `delta.compressionCodec`).
650 fn supports_table_compression(&self) -> bool {
651 false
652 }
653
654 /// Whether the platform requires every column referenced by the partition
655 /// key to also appear in the table's PRIMARY KEY (or any UNIQUE
656 /// constraint).
657 ///
658 /// `PostgreSQL` enforces this with `SQLSTATE 0A000` ("unique constraint on
659 /// partitioned table must include all partitioning columns"). `T-SQL`,
660 /// `MySQL`, `ClickHouse`, `Snowflake`, and `DuckDB` do not impose this
661 /// restriction.
662 fn partition_requires_pk_includes_field(&self) -> bool {
663 false
664 }
665}
666
667#[cfg(test)]
668mod tests {
669 use super::*;
670 use crate::merge_shape::MergeRestrictions;
671
672 struct DefaultDialect;
673 impl DialectCapabilities for DefaultDialect {}
674
675 /// Assert default DML capability flags are conservative.
676 /// Parameters:
677 /// - d: Dialect capabilities implementation under test.
678 fn assert_default_dml_capabilities(d: &impl DialectCapabilities) {
679 assert_eq!(d.merge_shape(), MergeShape::None);
680 assert!(!d.supports_on_conflict());
681 assert!(!d.supports_insert_overwrite());
682 assert!(!d.supports_update_from());
683 assert!(!d.supports_returning());
684 }
685
686 /// Assert default query capability flags and ANSI-safe behavior.
687 /// Parameters:
688 /// - d: Dialect capabilities implementation under test.
689 fn assert_default_query_capabilities(d: &impl DialectCapabilities) {
690 assert!(d.supports_cte());
691 assert!(d.supports_recursive_cte());
692 assert!(d.supports_window_functions());
693 assert!(d.supports_named_windows());
694 assert!(!d.supports_qualify());
695 assert!(!d.supports_groups_frame());
696 assert!(d.supports_nulls_ordering());
697 }
698
699 /// Assert default DDL capability and type flags are conservative.
700 /// Parameters:
701 /// - d: Dialect capabilities implementation under test.
702 fn assert_default_ddl_and_type_capabilities(d: &impl DialectCapabilities) {
703 assert!(!d.supports_identity());
704 assert!(!d.supports_sequences());
705 assert!(d.supports_schemas());
706 assert!(!d.supports_bulk_load());
707 assert!(!d.supports_stored_procedures());
708 // The two routine-form questions default the other way on purpose:
709 // answering `false` REFUSES a build, so it is never the answer an
710 // unknown platform falls into.
711 assert!(d.supports_stored_functions());
712 assert!(d.supports_table_functions());
713 assert!(!d.supports_tasks());
714 assert!(d.supports_partitioning());
715 assert!(d.supports_materialized_views());
716 assert!(d.supports_generated_columns());
717 assert!(d.supports_json());
718 assert!(!d.supports_unsigned_integers());
719 assert!(!d.supports_arrays());
720 assert!(d.supports_boolean());
721 }
722
723 /// Assert default platform limits and storage capability flags.
724 /// Parameters:
725 /// - d: Dialect capabilities implementation under test.
726 fn assert_default_limits_and_storage_capabilities(d: &impl DialectCapabilities) {
727 assert_eq!(d.default_schema(), "public");
728 assert_eq!(d.max_identifier_length(), 63);
729 assert_eq!(d.max_columns(), 10000);
730 assert_eq!(d.max_parameters(), 0);
731 assert!(!d.supports_partition_function());
732 assert!(!d.supports_partition_split_merge());
733 assert!(!d.supports_columnstore_index());
734 assert!(!d.supports_clustering_key());
735 assert!(!d.supports_table_compression());
736 }
737
738 #[test]
739 fn defaults_are_conservative() {
740 let d = DefaultDialect;
741 assert_default_dml_capabilities(&d);
742 assert_default_query_capabilities(&d);
743 assert_default_ddl_and_type_capabilities(&d);
744 assert_default_limits_and_storage_capabilities(&d);
745 }
746
747 #[test]
748 fn overrides_work() {
749 struct PostgresLike;
750 impl DialectCapabilities for PostgresLike {
751 fn merge_shape(&self) -> MergeShape {
752 MergeShape::Merge(MergeRestrictions::none())
753 }
754 fn supports_on_conflict(&self) -> bool {
755 true
756 }
757 fn supports_returning(&self) -> bool {
758 true
759 }
760 fn supports_identity(&self) -> bool {
761 true
762 }
763 fn supports_sequences(&self) -> bool {
764 true
765 }
766 fn supports_arrays(&self) -> bool {
767 true
768 }
769 fn max_columns(&self) -> usize {
770 1600
771 }
772 }
773
774 let d = PostgresLike;
775 assert!(d.merge_shape().merges());
776 assert!(d.supports_on_conflict());
777 assert!(d.supports_returning());
778 assert!(d.supports_identity());
779 assert!(d.supports_sequences());
780 assert!(d.supports_arrays());
781 assert_eq!(d.max_columns(), 1600);
782 // Untouched defaults
783 assert!(!d.supports_insert_overwrite());
784 assert!(d.supports_cte());
785 }
786
787 #[test]
788 fn new_capability_defaults_are_conservative() {
789 let d = DefaultDialect;
790 // Drop capabilities
791 assert!(d.supports_cascade_drop()); // true = ANSI default
792 assert!(!d.supports_function_overloading());
793 assert!(!d.supports_sync_drop());
794 assert!(!d.supports_force_drop());
795 // DML extended
796 assert!(!d.supports_compound_assignment());
797 assert!(!d.supports_window_frame_exclude());
798 assert!(!d.supports_merge_not_matched_by_source());
799 assert_eq!(d.merge_not_matched_keyword(), "WHEN NOT MATCHED");
800 assert!(!d.supports_merge_star_syntax());
801 assert!(!d.uses_alter_table_update());
802 assert!(!d.supports_multi_table_insert());
803 // DDL / Namespace
804 assert_eq!(d.schema_namespace_keyword(), "SCHEMA");
805 }
806
807 /// The default transaction answer is the ANSI one: a load can be wrapped,
808 /// in the standard spelling, and clearing a staging table stays inside the
809 /// wrap. Every platform that differs says so on its own dialect.
810 #[test]
811 fn the_default_load_wrap_is_the_ansi_one() {
812 let d = DefaultDialect;
813 assert!(d.supports_multi_statement_transactions());
814 assert_eq!(d.begin_transaction_keyword(), "START TRANSACTION");
815 assert_eq!(d.commit_transaction_keyword(), "COMMIT");
816 assert!(!d.truncate_commits_implicitly());
817 }
818
819 /// **A bound on the digits before the point is its own refusal**, and the
820 /// nearest type that fits keeps the scale and gives up whole digits.
821 #[test]
822 fn digits_before_the_point_have_their_own_bound() {
823 let bignumeric = DecimalLimits {
824 max_precision: 76,
825 max_scale: 38,
826 scale_within_precision: true,
827 max_integer_digits: Some(38),
828 };
829 assert_eq!(bignumeric.misfit(38, 0), None);
830 assert_eq!(bignumeric.misfit(76, 38), None);
831 assert_eq!(
832 bignumeric.misfit(39, 0).as_deref(),
833 Some("its decimals hold at most 38 digits before the point, and this asks for 39")
834 );
835 assert_eq!(bignumeric.nearest(39, 0), (38, 0));
836 assert_eq!(bignumeric.nearest(60, 10), (48, 10));
837 assert_eq!(bignumeric.nearest(99, 40), (76, 38));
838 for (precision, scale) in [(39, 0), (60, 10), (99, 40), (5, 6), (0, 0)] {
839 let (p, s) = bignumeric.nearest(precision, scale);
840 assert_eq!(bignumeric.misfit(p, s), None, "nearest({precision}, {scale}) fits");
841 }
842
843 let unbounded = DecimalLimits { max_integer_digits: None, ..bignumeric };
844 assert_eq!(unbounded.misfit(76, 0), None, "the digits in all are the only bound");
845 }
846}