Skip to main content

squonk_ast/ast/
ty.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! SQL type-name AST nodes shared by casts, DDL, parameters, and temporal syntax.
5
6use super::{Extension, Ident, Literal, NoExt, ObjectName};
7use crate::vocab::Meta;
8use thin_vec::ThinVec;
9
10/// SQL data types in the M1 AST surface.
11///
12/// Variants intentionally keep spelling tags where common dialects spell the
13/// same canonical type differently (`INT` vs `INTEGER`, `VARCHAR` vs
14/// `CHARACTER VARYING`, `BOOL` vs `BOOLEAN`). Tier-1 rendering can therefore
15/// round-trip the parsed surface, while dialect-target rendering can normalize
16/// to a target spelling.
17///
18/// Dialect-only type names live in the same closed enum (a type is one
19/// canonical shape, not a per-dialect tree). Their *recognition* is gated
20/// by [`TypeNameSyntax`](crate::dialect::TypeNameSyntax) data, so a name like
21/// `TINYINT` resolves to its built-in variant only under a dialect that opts in;
22/// elsewhere the same word falls through to [`UserDefined`](Self::UserDefined).
23///
24/// The [`Other(X)`](Self::Other) seam (ADR-0009) lets an out-of-tree consumer carry a
25/// host-owned type node produced by the parser crate's `Dialect::parse_data_type_hook`
26/// without spelling it as a stock variant or forcing it into
27/// [`UserDefined`](Self::UserDefined). Stock builtins use `X = NoExt` (uninhabited), so
28/// the arm is statically dead and `DataType` (= `DataType<NoExt>`) keeps its node size.
29#[derive(Clone, Debug, PartialEq, Eq, Hash)]
30#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
31#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
32pub enum DataType<X: Extension = NoExt> {
33    /// A boolean type (`BOOLEAN`/`BOOL`).
34    Boolean {
35        /// Exact source spelling retained for faithful rendering.
36        spelling: BooleanTypeName,
37        /// Source location and node identity.
38        meta: Meta,
39    },
40    /// MySQL `TINYINT` (1-byte integer); recognized only under
41    /// [`TypeNameSyntax::extended_scalar_type_names`](crate::dialect::TypeNameSyntax).
42    TinyInt {
43        /// Optional integer display width `(M)`, e.g. `TINYINT(1)`. This is *display
44        /// metadata* — it governs the left-pad width under `ZEROFILL` — never the
45        /// stored value's precision or range (`INT(1)` still stores the full 32-bit
46        /// range). Canonically MySQL's (`INT(11)`-style, deprecated in 8.0.17+ but
47        /// ubiquitous in dumps); SQLite accepts it through affinity type-name
48        /// absorption. Gated by
49        /// [`TypeNameSyntax::integer_display_width`](crate::dialect::TypeNameSyntax),
50        /// so ANSI/PostgreSQL reject the parenthesized form on a built-in integer.
51        display_width: Option<u32>,
52        /// Source location and node identity.
53        meta: Meta,
54    },
55    /// A `SMALLINT` (2-byte integer).
56    SmallInt {
57        /// Optional display width for this syntax.
58        display_width: Option<u32>,
59        /// Source location and node identity.
60        meta: Meta,
61    },
62    /// MySQL `MEDIUMINT` (3-byte integer); recognized only under
63    /// [`TypeNameSyntax::extended_scalar_type_names`](crate::dialect::TypeNameSyntax).
64    MediumInt {
65        /// Optional display width for this syntax.
66        display_width: Option<u32>,
67        /// Source location and node identity.
68        meta: Meta,
69    },
70    /// An `INT`/`INTEGER` (4-byte integer).
71    Integer {
72        /// Exact source spelling retained for faithful rendering.
73        spelling: IntegerTypeName,
74        /// Optional display width for this syntax.
75        display_width: Option<u32>,
76        /// Source location and node identity.
77        meta: Meta,
78    },
79    /// A `BIGINT` (8-byte integer).
80    BigInt {
81        /// Optional display width for this syntax.
82        display_width: Option<u32>,
83        /// Source location and node identity.
84        meta: Meta,
85    },
86    /// An exact `DECIMAL`/`NUMERIC(p, s)` type.
87    Decimal {
88        /// Exact source spelling retained for faithful rendering.
89        spelling: DecimalTypeName,
90        /// The precision modifier. Signed because PostgreSQL parses the `numeric`/`decimal`
91        /// type-modifier arguments as a general expression list at raw-parse time, so a signed
92        /// modifier (`numeric(-3, 6)`) is accepted and validated only later — the sign is gated
93        /// for acceptance by
94        /// [`TypeNameSyntax::signed_type_modifier`](crate::dialect::TypeNameSyntax); off
95        /// elsewhere a leading `-` is a clean parse error.
96        precision: Option<i32>,
97        /// The scale modifier. Signed for the same reason as [`precision`](Self::Decimal::precision):
98        /// PostgreSQL accepts a negative scale (`numeric(5, -2)` rounds to tens), which the
99        /// standard/MySQL reject.
100        scale: Option<i32>,
101        /// Source location and node identity.
102        meta: Meta,
103    },
104    /// A `FLOAT[(p)]` approximate numeric type.
105    Float {
106        /// Numeric precision specified by this syntax.
107        precision: Option<u32>,
108        /// Source location and node identity.
109        meta: Meta,
110    },
111    /// A `REAL` (single-precision floating-point) type.
112    Real {
113        /// Source location and node identity.
114        meta: Meta,
115    },
116    /// A `DOUBLE PRECISION` (double-precision floating-point) type.
117    Double {
118        /// Exact source spelling retained for faithful rendering.
119        spelling: DoubleTypeName,
120        /// Source location and node identity.
121        meta: Meta,
122    },
123    /// A variable-length character LOB. The spelling tag carries the MySQL size
124    /// family (`TINYTEXT`/`MEDIUMTEXT`/`LONGTEXT`), which differ only in declared
125    /// maximum length; the bare PostgreSQL/MySQL `TEXT` is the default spelling.
126    Text {
127        /// Exact source spelling retained for faithful rendering.
128        spelling: TextTypeName,
129        /// MySQL's character-set type annotation — the whole TEXT size family admits it in
130        /// column position (engine-measured on mysql:8.4: `TEXT CHARACTER SET utf8mb4`,
131        /// `TINYTEXT ASCII`, `LONGTEXT BINARY`, `TEXT BYTE` all prepare). See the field
132        /// docs on [`Character`](Self::Character); `None` outside MySQL.
133        charset: Option<Box<CharsetAnnotation>>,
134        /// Source location and node identity.
135        meta: Meta,
136    },
137    /// A binary LOB family: MySQL `BLOB`/`TINYBLOB`/`MEDIUMBLOB`/`LONGBLOB`. The
138    /// binary analog of [`Text`](Self::Text); recognized only under
139    /// [`TypeNameSyntax::extended_scalar_type_names`](crate::dialect::TypeNameSyntax).
140    Blob {
141        /// Exact source spelling retained for faithful rendering.
142        spelling: BlobTypeName,
143        /// Source location and node identity.
144        meta: Meta,
145    },
146    /// A fixed/variable-length character type (`CHAR`/`VARCHAR`/`CHARACTER`/…).
147    Character {
148        /// Exact source spelling retained for faithful rendering.
149        spelling: CharacterTypeName,
150        /// Optional size for this syntax.
151        size: Option<u32>,
152        /// MySQL's character-set type annotation — the grammar's
153        /// `opt_charset_with_opt_binary` production: `CHARACTER SET <name>` (or the
154        /// `CHARSET` synonym), the `ASCII`/`UNICODE`/`BYTE` shortcuts, and/or the trailing
155        /// `BINARY` binary-collation modifier. Part of the *type*, not a column attribute:
156        /// it must directly follow the type (and its length) and is rejected once any
157        /// column attribute intervenes (`CHAR(5) NOT NULL CHARACTER SET x` is an
158        /// `ER_PARSE_ERROR` on mysql:8), which is why it lives on the type node — unlike
159        /// `COLLATE`, a free-floating column attribute. Recognized only under
160        /// [`TypeNameSyntax::character_set_annotation`](crate::dialect::TypeNameSyntax),
161        /// and only on the non-national spellings (`CHAR`/`CHARACTER`/`VARCHAR`): the
162        /// national forms (`NCHAR`/`NATIONAL CHAR`) fix their own charset and reject the
163        /// annotation, so they always carry `None`. `None` for every non-MySQL char type.
164        ///
165        /// Boxed: the annotation is a rare, fat payload (MySQL-only, and only on annotated
166        /// char types), so ADR-0007 keeps it off the hot [`DataType`] node's inline width —
167        /// the box is paid only on the rare annotated path.
168        charset: Option<Box<CharsetAnnotation>>,
169        /// Source location and node identity.
170        meta: Meta,
171    },
172    /// A fixed/variable-length binary type (`BINARY`/`VARBINARY`).
173    Binary {
174        /// Exact source spelling retained for faithful rendering.
175        spelling: BinaryTypeName,
176        /// Optional size for this syntax.
177        size: Option<u32>,
178        /// Source location and node identity.
179        meta: Meta,
180    },
181    /// A bit-string type: `BIT [VARYING] [(n)]` (PostgreSQL `bit`/`varbit`).
182    Bit {
183        /// `BIT VARYING` (variable-length) vs fixed-length `BIT`.
184        varying: bool,
185        /// Optional size for this syntax.
186        size: Option<u32>,
187        /// Source location and node identity.
188        meta: Meta,
189    },
190    /// The PostgreSQL `JSON` type (distinct from the `jsonb` user-defined name).
191    Json {
192        /// Source location and node identity.
193        meta: Meta,
194    },
195    /// The `UUID` type (PostgreSQL `uuid`, DuckDB `UUID`). A single-keyword scalar
196    /// carrying no parameters, shaped exactly like [`Json`](Self::Json). Recognition is
197    /// *ungated* — the word resolves to this canonical variant wherever a type name is
198    /// admitted, so a type planner reads one UUID identity regardless of dialect rather
199    /// than a per-dialect [`UserDefined`](Self::UserDefined) name. This is *identity*, not
200    /// *acceptance*: dialects that reject `UUID` in a given position still reject it there
201    /// (e.g. MySQL's narrow `CAST` target set excludes `UUID`), the same way `JSON` is a
202    /// first-class variant yet remains subject to each dialect's positional rules.
203    Uuid {
204        /// Source location and node identity.
205        meta: Meta,
206    },
207    /// A `DATE` type.
208    Date {
209        /// Source location and node identity.
210        meta: Meta,
211    },
212    /// A `TIME [WITH|WITHOUT TIME ZONE]` type.
213    Time {
214        /// Exact source spelling retained for faithful rendering.
215        spelling: TimeTypeName,
216        /// Numeric precision specified by this syntax.
217        precision: Option<u32>,
218        /// The `WITH`/`WITHOUT TIME ZONE` qualifier; see [`TimeZone`].
219        time_zone: TimeZone,
220        /// Source location and node identity.
221        meta: Meta,
222    },
223    /// A `TIMESTAMP [WITH|WITHOUT TIME ZONE]` type.
224    Timestamp {
225        /// Exact source spelling retained for faithful rendering.
226        spelling: TimestampTypeName,
227        /// Numeric precision specified by this syntax.
228        precision: Option<u32>,
229        /// The `WITH`/`WITHOUT TIME ZONE` qualifier; see [`TimeZone`].
230        time_zone: TimeZone,
231        /// Source location and node identity.
232        meta: Meta,
233    },
234    /// An `INTERVAL` type, with an optional field qualifier and precision.
235    Interval {
236        /// Optional fields for this syntax.
237        fields: Option<IntervalFields>,
238        /// Numeric precision specified by this syntax.
239        precision: Option<u32>,
240        /// Source location and node identity.
241        meta: Meta,
242    },
243    /// `ENUM('a', 'b', ...)`: a string type constrained to a value list — MySQL's column
244    /// type and DuckDB's `x::ENUM('a', 'b')` cast target. Structural (it carries the value
245    /// list), so it is one canonical variant rather than a spelling tag; recognized under
246    /// [`TypeNameSyntax::enum_type`](crate::dialect::TypeNameSyntax). The values are string
247    /// [`Literal`]s whose spelling round-trips from their span.
248    Enum {
249        /// Values in source order.
250        values: ThinVec<Literal>,
251        /// MySQL's character-set type annotation, which `ENUM` admits in column position
252        /// (engine-measured on mysql:8.4: `ENUM('a') CHARACTER SET utf8mb4` / `ASCII` /
253        /// `BYTE` / `BINARY` all prepare). See the field docs on
254        /// [`Character`](Self::Character); always `None` for DuckDB (whose grammar has no
255        /// such annotation) and outside MySQL.
256        charset: Option<Box<CharsetAnnotation>>,
257        /// Source location and node identity.
258        meta: Meta,
259    },
260    /// MySQL `SET('a', 'b', ...)`: a string type whose value is a subset of the
261    /// listed members, recognized under
262    /// [`TypeNameSyntax::set_type`](crate::dialect::TypeNameSyntax). Shares
263    /// [`Enum`](Self::Enum)'s value-list shape; the two stay distinct variants because the
264    /// membership semantics differ.
265    Set {
266        /// Values in source order.
267        values: ThinVec<Literal>,
268        /// MySQL's character-set type annotation — same surface as [`Enum`](Self::Enum)
269        /// (engine-measured on mysql:8.4).
270        charset: Option<Box<CharsetAnnotation>>,
271        /// Source location and node identity.
272        meta: Meta,
273    },
274    /// A numeric type carrying MySQL's `SIGNED`/`UNSIGNED`/`ZEROFILL` modifiers,
275    /// gated by [`TypeNameSyntax::numeric_modifiers`](crate::dialect::TypeNameSyntax).
276    ///
277    /// One wrapper models the modifier for every numeric inner type, so the flag is
278    /// kept once rather than duplicated onto each numeric variant — the
279    /// same wrap-the-inner-type shape as [`Array`](Self::Array). `element` is
280    /// optional because MySQL's `CAST(x AS UNSIGNED)` / `CAST(x AS SIGNED)` use the
281    /// modifier as a standalone integer cast target that names no base type.
282    NumericModifier {
283        /// Optional element for this syntax.
284        element: Option<Box<DataType<X>>>,
285        /// The `SIGNED`/`UNSIGNED` modifier; see [`Signedness`].
286        signedness: Signedness,
287        /// Whether the zerofill form was present in the source.
288        zerofill: bool,
289        /// Source location and node identity.
290        meta: Meta,
291    },
292    /// An array type (`T[]`, `T[n]`, or `T ARRAY[n]`).
293    Array {
294        /// The array element type.
295        element: Box<DataType<X>>,
296        /// Fixed cardinality for the DuckDB fixed-size `ARRAY` (`INTEGER[3]` /
297        /// `INTEGER ARRAY[3]`), or `None` for the variable-length list (`INTEGER[]` /
298        /// `INTEGER ARRAY`). The two are distinct DuckDB types (`ARRAY` enforces the
299        /// count, `LIST` is unbounded); PostgreSQL accepts the bound syntactically but
300        /// ignores it (`int[3]` binds identically to `int[]`), so the value is retained
301        /// for round-trip regardless of dialect and its enforcement is a consumer concern.
302        size: Option<u32>,
303        /// Bracket `T[]`/`T[n]` vs keyword `T ARRAY`/`T ARRAY[n]` surface. One canonical
304        /// array-type shape covers both spellings; the tag round-trips the
305        /// written form.
306        spelling: ArrayTypeSpelling,
307        /// Source location and node identity.
308        meta: Meta,
309    },
310    /// An anonymous composite (record) type: DuckDB `STRUCT(a INTEGER, b VARCHAR)` or
311    /// the standard `ROW(...)` spelling of the same shape. One canonical named-field
312    /// list covers both spellings; `spelling` records the written keyword.
313    /// Recognized only under
314    /// [`TypeNameSyntax::composite_types`](crate::dialect::TypeNameSyntax); ANSI/
315    /// PostgreSQL reject the anonymous form (PostgreSQL has only *named* composite
316    /// types and spells `ROW` as a value constructor, never a type).
317    Struct {
318        /// fields in source order.
319        fields: ThinVec<StructTypeField<X>>,
320        /// Exact source spelling retained for faithful rendering.
321        spelling: StructTypeSpelling,
322        /// Source location and node identity.
323        meta: Meta,
324    },
325    /// An anonymous tagged-union type: DuckDB `UNION(tag1 T1, tag2 T2, ...)`. Shares the
326    /// named-member shape of [`Struct`](Self::Struct) but stays a distinct variant
327    /// because the semantics differ (a tagged sum type vs a product type), exactly as
328    /// [`Enum`](Self::Enum)/[`Set`](Self::Set) share a value-list shape yet stay
329    /// distinct. Gated by the same `composite_types` flag.
330    Union {
331        /// members in source order.
332        members: ThinVec<StructTypeField<X>>,
333        /// Source location and node identity.
334        meta: Meta,
335    },
336    /// An anonymous map type: DuckDB `MAP(K, V)`, where `K` and `V` are themselves
337    /// types (`MAP(VARCHAR, STRUCT(x INTEGER))`, `MAP(INTEGER[], VARCHAR)`). DuckDB
338    /// desugars a map to a `LIST` of `STRUCT(key, value)` in its serialized tree; the
339    /// canonical shape keeps the written key/value types directly (the desugaring is a
340    /// representation-equivalent difference the structural oracle normalizes, like the
341    /// `list_value`/`struct_pack` value desugars). Gated by `composite_types`.
342    Map {
343        /// The map's key type.
344        key: Box<DataType<X>>,
345        /// Value supplied by this syntax.
346        value: Box<DataType<X>>,
347        /// Source location and node identity.
348        meta: Meta,
349    },
350    /// A ClickHouse parametric type combinator wrapping exactly one inner type:
351    /// `Nullable(T)` (and the sibling `LowCardinality(T)`, which shares this exact
352    /// single-inner-type shape). One canonical variant with a [`WrappedTypeKind`] axis
353    /// models the whole wrapper-shaped family — the same wrap-one-inner-type idiom as
354    /// [`NumericModifier`](Self::NumericModifier), which keeps its modifier flag once
355    /// rather than duplicating a variant per numeric inner type — so a further
356    /// wrapper keyword is a new [`WrappedTypeKind`] arm plus a parser branch, never a
357    /// new `DataType` variant or `Render` arm.
358    ///
359    /// `inner` is a full, recursively-nested type, so `Nullable(DECIMAL(10, 2))` and
360    /// `Nullable(String)[]` both parse. ClickHouse constrains *composability* only at
361    /// bind time — it rejects `Nullable(Nullable(T))` and `Nullable(Array(T))` with a
362    /// type-resolution `DB::Exception`, not a grammar error — so those nestings
363    /// parse-accept here and the constraint is a binder concern, the same parse-vs-bind
364    /// split as ragged array literals.
365    Wrapped {
366        /// Which wrapper (`Nullable`/`LowCardinality`); see [`WrappedTypeKind`].
367        kind: WrappedTypeKind,
368        /// The wrapped inner type.
369        inner: Box<DataType<X>>,
370        /// Source location and node identity.
371        meta: Meta,
372    },
373    /// ClickHouse `FixedString(N)`: a fixed-length byte string of exactly `N` bytes.
374    ///
375    /// Deliberately its own variant rather than a [`WrappedTypeKind`](Self::Wrapped) arm:
376    /// its argument is a scalar length `N`, not a nested inner type, so it does not share
377    /// the single-inner-type wrapper shape. It instead reuses the length-carrying string
378    /// idiom of [`Character`](Self::Character)/[`Binary`](Self::Binary), but with the
379    /// length as a **non-optional** `u32` rather than `Option<u32>`: ClickHouse's grammar
380    /// makes `N` mandatory (a bare `FixedString` with no parens is a different, invalid
381    /// spelling that falls through to the user-defined-type path), so the required argument
382    /// is encoded in the type itself instead of a runtime `None` those spelling-tagged
383    /// variants must tolerate. That mandatory-scalar shape is why it is not folded onto
384    /// [`Character`](Self::Character) under a spelling tag — and `FixedString` is a *byte*
385    /// string carrying no charset annotation, unlike the character variants.
386    ///
387    /// `N` is a positive integer in ClickHouse; the grammar admits any `u32` literal (`0`
388    /// included) and ClickHouse rejects `FixedString(0)` only at type resolution, the same
389    /// parse-vs-bind split as the `Wrapped` composability rejects. Recognized only under
390    /// [`TypeNameSyntax::fixed_string_type`](crate::dialect::TypeNameSyntax); the canonical
391    /// render emits ClickHouse's mixed-case spelling (`FixedString`, never `FIXEDSTRING`).
392    FixedString {
393        /// Length bound specified by this syntax.
394        length: u32,
395        /// Source location and node identity.
396        meta: Meta,
397    },
398    /// ClickHouse `DateTime64(P[, 'timezone'])`: a sub-second timestamp with `P` fractional
399    /// digits of precision and an optional IANA time-zone name.
400    ///
401    /// Its own variant rather than a spelling tag on [`Timestamp`](Self::Timestamp), for two
402    /// shape reasons that mirror [`FixedString`](Self::FixedString)'s mandatory-scalar
403    /// argument. First, precision is **mandatory**: ClickHouse's `DateTime64` has no bare
404    /// spelling (the `(P)` is required; a bare `DateTime64` with no parens is a different
405    /// spelling that falls through to the user-defined-type path), so `precision` is a
406    /// non-optional `u32` rather than the `Option<u32>` that the ANSI/MySQL
407    /// [`Timestamp`](Self::Timestamp)/[`Time`](Self::Time) variants carry. Second, its time
408    /// zone is a single-quoted string-literal *argument*, not the ANSI `WITH TIME ZONE`
409    /// flag those variants encode as a [`TimeZone`] tag — a different surface that would not
410    /// fit the tag. Those two differences are why it is not folded onto `Timestamp`.
411    ///
412    /// `timezone` is the optional second argument, held as the source-spelled string
413    /// [`Literal`] so its exact quoting round-trips (the [`Enum`](Self::Enum) value-member
414    /// idiom). `P` is parsed as any `u32` literal; ClickHouse's documented `0..=9` range is a
415    /// bind-time reject, not a grammar error — the same parse-vs-bind split as
416    /// [`FixedString`](Self::FixedString)'s positive-length rule. Recognized only under
417    /// [`TypeNameSyntax::datetime64_type`](crate::dialect::TypeNameSyntax); the canonical
418    /// render emits ClickHouse's mixed-case spelling (`DateTime64`, never `DATETIME64`).
419    ///
420    /// The timezone-only sibling `DateTime('timezone')` (no precision, on the plain
421    /// `DateTime` type) is a distinct surface and out of scope here — see the deferral on
422    /// the owning ticket.
423    DateTime64 {
424        /// Numeric precision specified by this syntax.
425        precision: u32,
426        /// Optional timezone for this syntax.
427        timezone: Option<Box<Literal>>,
428        /// Source location and node identity.
429        meta: Meta,
430    },
431    /// ClickHouse `Nested(name1 Type1, name2 Type2, ...)`: a named-field composite that
432    /// models a repeated group — semantically a set of parallel same-length arrays sharing
433    /// a common column prefix, not a single record value.
434    ///
435    /// Shares the named-field [`StructTypeField`] shape of [`Struct`](Self::Struct)/
436    /// [`Union`](Self::Union) but stays a distinct variant because the semantics differ (a
437    /// repeated nested structure vs a product or a tagged sum), exactly as
438    /// [`Union`](Self::Union) shares `Struct`'s field shape yet stays distinct and
439    /// [`Enum`](Self::Enum)/[`Set`](Self::Set) share a value-list shape yet stay distinct.
440    /// A spelling tag on `Struct` would conflate those semantics and force the
441    /// `composite_types` gate; `Nested` instead carries its own
442    /// [`TypeNameSyntax::nested_type`](crate::dialect::TypeNameSyntax) flag
443    /// (one behaviour = one flag) and its canonical render re-emits `Nested`, never `STRUCT`.
444    ///
445    /// A field type is a full, recursively-nested type, so `Nested(x Nested(y UInt8))`
446    /// parses: ClickHouse permits arbitrary nesting levels (under `flatten_nested=0`), and
447    /// any level limit is a setting/bind concern, not a grammar error — the same parse-vs-bind
448    /// split as the [`Wrapped`](Self::Wrapped) composability rejects. At least one field is
449    /// required (a bare `Nested()` is a syntax error), enforced by the one-or-more field list.
450    Nested {
451        /// fields in source order.
452        fields: ThinVec<StructTypeField<X>>,
453        /// Source location and node identity.
454        meta: Meta,
455    },
456    /// ClickHouse's fixed-bit-width integer type names: the signed `Int8`/`Int16`/`Int32`/
457    /// `Int64`/`Int128`/`Int256` family and their unsigned `UInt8`…`UInt256` siblings.
458    ///
459    /// One canonical variant carries the signedness ([`signed`](Self::FixedWidthInt::signed))
460    /// and the width ([`IntWidth`]) as *data*, rather than a variant per width — the whole
461    /// bit-width family travels together in a dialect exactly as MySQL's
462    /// `TINYINT`/`MEDIUMINT`/… ride one recognition gate, so it is one behaviour and one
463    /// [`DataType`] shape (the [`extended_scalar_type_names`](crate::dialect::TypeNameSyntax)
464    /// precedent), gated by
465    /// [`TypeNameSyntax::bit_width_integer_names`](crate::dialect::TypeNameSyntax). Signedness
466    /// and width are the only axes, so the whole family is one `DataType` shape and one
467    /// `Render` arm rather than twelve.
468    ///
469    /// Kept distinct from [`Integer`](Self::Integer)/[`BigInt`](Self::BigInt) rather than
470    /// folded under a spelling tag: `Int32`/`Int64` name the same underlying types as `INT`
471    /// and `BIGINT` but round-trip their own written surface, and the narrower/wider widths
472    /// (`Int8`/`Int128`/`Int256`) have no ANSI integer variant at all. The names take no
473    /// arguments, so a bare `Int256` off-gate is an ordinary user-defined type name (the
474    /// trivial off-gate boundary, like a bare `Nullable`), never a parse error. The canonical
475    /// render emits ClickHouse's mixed-case spelling (`Int256`/`UInt256`, never `INT256`).
476    FixedWidthInt {
477        /// `true` for the signed `Int*` spellings, `false` for the unsigned `UInt*`.
478        signed: bool,
479        /// The integer bit width (`Int8`…`Int256`); see [`IntWidth`].
480        width: IntWidth,
481        /// Source location and node identity.
482        meta: Meta,
483    },
484    /// A user-defined (or otherwise non-built-in) type reference — a dot-separated
485    /// qualified [`ObjectName`] with an optional parenthesized modifier list. Every type
486    /// name no typed variant claims lands here (the FALLBACK path), so a bare `GEOMETRY`,
487    /// `citext`, or `schema.mytype` is a `UserDefined`.
488    UserDefined {
489        /// Name referenced by this syntax.
490        name: ObjectName,
491        /// The parenthesized modifier arguments, as constant [`Literal`]s whose spelling
492        /// round-trips from their span. An unsigned-integer modifier (`FOO(3)`) parses
493        /// under every dialect; a string-literal modifier (`GEOMETRY('OGC:CRS84')`) is
494        /// DuckDB's, admitted only under
495        /// [`TypeNameSyntax::string_type_modifiers`](crate::dialect::TypeNameSyntax).
496        /// Empty when the name carries no parenthesized list.
497        modifiers: ThinVec<Literal>,
498        /// Source location and node identity.
499        meta: Meta,
500    },
501    /// SQLite's liberal affinity type name — a free run of one-or-more space-separated
502    /// words (`UNSIGNED BIG INT`, `LONG INTEGER`, `NATIVE CHARACTER`, and even the
503    /// misspelled `INTEGEB PRIMARI KEY`) with an optional one-or-two-argument parenthesized
504    /// modifier (`VARCHAR(123,456)`, `FLOATING POINT(5,10)`). SQLite has no fixed type
505    /// vocabulary — a column/cast type is any `ids ...` token run terminated by a
506    /// column-constraint keyword, a comma, or a close paren — so a multi-word or two-argument
507    /// name that no typed variant can faithfully hold lands here instead of over-rejecting.
508    /// Recognized only under
509    /// [`TypeNameSyntax::liberal_type_names`](crate::dialect::TypeNameSyntax) (SQLite +
510    /// Lenient).
511    ///
512    /// # Why its own variant, not [`UserDefined`](Self::UserDefined)
513    ///
514    /// `UserDefined` carries an [`ObjectName`] — a *dot*-separated qualified name
515    /// (`schema.type`) that renders with `.` — and its `modifiers` are unsigned `u32`s. A
516    /// liberal affinity name is a *space*-separated word run with a distinct render, so it is
517    /// a genuinely different canonical shape (ADR-0011: one type = one shape), not a spelling
518    /// of the same `UserDefined` reference. Folding it in would force a render branch and a
519    /// separator tag onto the common single-word user-defined path and grow its hot node; a
520    /// dedicated variant keeps `UserDefined` untouched. A bare single-word affinity name
521    /// (`BANANA`) still parses to `UserDefined` — this variant is reached only when a second
522    /// word or a two-argument paren list makes the typed / user-defined parse insufficient
523    /// (the FALLBACK ordering: typed variants win wherever they can faithfully represent the
524    /// input, so `INT`, `DOUBLE PRECISION`, `VARCHAR(255)`, `NATIONAL CHARACTER(15)` keep
525    /// their typed variants under SQLite).
526    ///
527    /// # Boundary (engine-probed, rusqlite/sqlite3 3.53.2 & 3.43.2)
528    ///
529    /// The word run terminates at a column-constraint keyword — `PRIMARY`, `NOT`, `NULL`,
530    /// `UNIQUE`, `CHECK`, `DEFAULT`, `COLLATE`, `REFERENCES`, `CONSTRAINT`, `AS`, `GENERATED`
531    /// (each engine-probed as a terminator, e.g. `x MY PRIMARY` rejects because `PRIMARY`
532    /// starts `PRIMARY KEY`, not the type) — or a comma / close paren; every other
533    /// identifier-or-non-reserved-keyword word is absorbed (`x FOO BAR BAZ QUX` accepts). A
534    /// reserved word (`SELECT`) can never be a type word (`x SELECT` rejects). The optional
535    /// paren list holds at most two arguments (`FOO(1,2)` accepts, `FOO(1,2,3)` rejects).
536    Liberal {
537        /// The one-or-more space-separated affinity words, each an [`Ident`] whose quote
538        /// style round-trips (a quoted `"foo bar"` word counts as one). Rendered
539        /// space-separated.
540        words: ThinVec<Ident>,
541        /// The zero-, one-, or two-element parenthesized modifier list (`VARCHAR(123,456)`
542        /// carries `[123, 456]`). Empty when the name has no parens. Unsigned like
543        /// [`UserDefined::modifiers`](Self::UserDefined) — SQLite's grammar also admits a
544        /// signed or fractional argument, but the corpus surface is unsigned and the
545        /// signed/float forms are left to a follow-up (their absence under-accepts, never
546        /// over-accepts).
547        args: ThinVec<u32>,
548        /// Source location and node identity.
549        meta: Meta,
550    },
551    /// A host-owned type node an out-of-tree dialect parses through the parser crate's
552    /// `Dialect::parse_data_type_hook` (ADR-0009). The sixth `Other(X)` seam:
553    /// `X = NoExt` for every shipped builtin leaves this arm uninhabited and statically
554    /// dead, so stock parsing never produces it and `DataType<NoExt>` keeps its
555    /// byte-identical size.
556    Other {
557        /// The dialect extension node value.
558        ext: X,
559        /// Source location and node identity.
560        meta: Meta,
561    },
562}
563
564/// One named field of an anonymous composite type: `name TYPE`, as in a DuckDB
565/// `STRUCT(a INTEGER)` field or a `UNION(tag T)` member.
566///
567/// The field name is a genuine identifier position (bare `a`, or double-quoted
568/// `"key"`), so it is an [`Ident`] that round-trips its quote style, not a bare
569/// symbol. Mirrors the Expr-side `StructField`: a spanned sub-node with its own
570/// [`Meta`].
571#[derive(Clone, Debug, PartialEq, Eq, Hash)]
572#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
573#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
574pub struct StructTypeField<X: Extension = NoExt> {
575    /// Name referenced by this syntax.
576    pub name: Ident,
577    /// Data type named by this syntax.
578    pub ty: DataType<X>,
579    /// Source location and node identity.
580    pub meta: Meta,
581}
582
583/// How an anonymous composite type ([`DataType::Struct`]) was spelled.
584///
585/// DuckDB folds `STRUCT(...)` and the standard `ROW(...)` to the same canonical
586/// composite type; this tag records the written keyword so each round-trips,
587/// mirroring [`CastSyntax`](crate::ast::CastSyntax).
588#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
589#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
590#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
591pub enum StructTypeSpelling {
592    /// The `STRUCT(...)` keyword.
593    Struct,
594    /// The standard `ROW(...)` keyword.
595    Row,
596    /// BigQuery angle-bracket type form `STRUCT<field TYPE, …>` (type position).
597    AngleBracket,
598}
599
600/// The written surface of an array-type suffix ([`DataType::Array`]).
601///
602/// Bracket `T[]`/`T[n]` and keyword `T ARRAY`/`T ARRAY[n]` name the same canonical
603/// array-type shape; the tag round-trips the written form.
604#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
605#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
606#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
607pub enum ArrayTypeSpelling {
608    /// The bracket suffix `T[]` / `T[n]`.
609    Bracket,
610    /// The `ARRAY` keyword suffix `T ARRAY` / `T ARRAY[n]`.
611    Keyword,
612    /// BigQuery angle-bracket type form `ARRAY<T>` (type position, prefix).
613    AngleBracket,
614}
615
616/// The parametric wrapper keyword of a [`DataType::Wrapped`] type combinator.
617///
618/// Each variant is a distinct ClickHouse type combinator that shares the
619/// single-inner-type wrapper shape; recognition of each is gated on its own
620/// [`TypeNameSyntax`](crate::dialect::TypeNameSyntax) flag (one behaviour = one flag),
621/// so a preset opts into each keyword independently rather than through one bundled
622/// gate. The canonical render emits ClickHouse's mixed-case spelling (`Nullable`).
623#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
624#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
625#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
626pub enum WrappedTypeKind {
627    /// `Nullable(T)` — the inner type extended with a `NULL` value.
628    Nullable,
629    /// `LowCardinality(T)` — a dictionary-encoding wrapper over the inner type,
630    /// transparent to query semantics (ClickHouse constrains which `T` at type
631    /// resolution; the grammar accepts any single inner type).
632    LowCardinality,
633}
634
635#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
636#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
637#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
638/// The SQL boolean type name forms represented by the AST.
639pub enum BooleanTypeName {
640    /// The `BOOLEAN` spelling.
641    Boolean,
642    /// The `BOOL` spelling.
643    Bool,
644}
645
646#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
647#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
648#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
649/// The SQL integer type name forms represented by the AST.
650pub enum IntegerTypeName {
651    /// The `INT` spelling.
652    Int,
653    /// The `INTEGER` spelling.
654    Integer,
655}
656
657/// The bit width spelled in a ClickHouse [`DataType::FixedWidthInt`] type name — the `N`
658/// of `Int<N>`/`UInt<N>`. ClickHouse defines exactly these six widths; the width is a
659/// closed axis (a precise enum, not a raw bit count) so only the real spellings are
660/// representable.
661#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
662#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
663#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
664pub enum IntWidth {
665    /// An 8-bit integer width.
666    W8,
667    /// A 16-bit integer width.
668    W16,
669    /// A 32-bit integer width.
670    W32,
671    /// A 64-bit integer width.
672    W64,
673    /// A 128-bit integer width.
674    W128,
675    /// A 256-bit integer width.
676    W256,
677}
678
679#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
680#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
681#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
682/// The SQL decimal type name forms represented by the AST.
683pub enum DecimalTypeName {
684    /// The `DECIMAL` spelling.
685    Decimal,
686    /// The `DEC` spelling.
687    Dec,
688    /// The `NUMERIC` spelling.
689    Numeric,
690}
691
692#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
693#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
694#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
695/// The SQL double type name forms represented by the AST.
696pub enum DoubleTypeName {
697    /// The standard `DOUBLE PRECISION` spelling.
698    DoublePrecision,
699    /// MySQL's bare `DOUBLE` floating-point type (PostgreSQL leaves bare `double`
700    /// unreserved, so it is recognized only under
701    /// [`TypeNameSyntax::extended_scalar_type_names`](crate::dialect::TypeNameSyntax)).
702    Double,
703}
704
705/// The MySQL character-LOB size family spelled by [`DataType::Text`]. They differ
706/// only in declared maximum length, so they share one variant tagged by spelling.
707#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
708#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
709#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
710pub enum TextTypeName {
711    /// The bare `TEXT` type (PostgreSQL; MySQL's mid-size member).
712    Text,
713    /// MySQL `TINYTEXT`.
714    TinyText,
715    /// MySQL `MEDIUMTEXT`.
716    MediumText,
717    /// MySQL `LONGTEXT`.
718    LongText,
719}
720
721/// The MySQL binary-LOB size family spelled by [`DataType::Blob`].
722#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
723#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
724#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
725pub enum BlobTypeName {
726    /// MySQL `BLOB`.
727    Blob,
728    /// MySQL `TINYBLOB`.
729    TinyBlob,
730    /// MySQL `MEDIUMBLOB`.
731    MediumBlob,
732    /// MySQL `LONGBLOB`.
733    LongBlob,
734}
735
736/// MySQL's numeric sign modifier, carried by [`DataType::NumericModifier`].
737///
738/// `ZEROFILL` (a separate flag on the wrapper) implies unsigned semantics in
739/// MySQL, but the written sign is preserved here so the surface round-trips: a
740/// bare `INT ZEROFILL` keeps [`Unspecified`](Self::Unspecified), not a synthesized
741/// `UNSIGNED`.
742#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
743#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
744#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
745pub enum Signedness {
746    /// No `SIGNED`/`UNSIGNED` keyword was written.
747    Unspecified,
748    /// `SIGNED`.
749    Signed,
750    /// `UNSIGNED`.
751    Unsigned,
752}
753
754#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
755#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
756#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
757/// The SQL character type name forms represented by the AST.
758pub enum CharacterTypeName {
759    /// The fixed-length `CHAR` spelling.
760    Char,
761    /// The fixed-length `CHARACTER` spelling.
762    Character,
763    /// The variable-length `CHAR VARYING` spelling.
764    CharVarying,
765    /// The variable-length `CHARACTER VARYING` spelling.
766    CharacterVarying,
767    /// The variable-length `VARCHAR` spelling.
768    Varchar,
769    /// `NCHAR` (national character).
770    Nchar,
771    /// `NCHAR VARYING`.
772    NcharVarying,
773    /// `NATIONAL CHAR`.
774    NationalChar,
775    /// `NATIONAL CHAR VARYING`.
776    NationalCharVarying,
777    /// `NATIONAL CHARACTER`.
778    NationalCharacter,
779    /// `NATIONAL CHARACTER VARYING`.
780    NationalCharacterVarying,
781}
782
783/// A MySQL character-set type annotation carried by the string-typed [`DataType`]
784/// variants — [`Character`](DataType::Character), the [`Text`](DataType::Text) LOB family,
785/// [`Enum`](DataType::Enum), and [`Set`](DataType::Set) (each engine-measured to admit it
786/// on mysql:8.4) — the grammar's `opt_charset_with_opt_binary` production, part of the
787/// *type* rather than a column attribute (see the field docs on
788/// [`DataType::Character`]).
789///
790/// The canonical render emits the charset selector first, then `BINARY`
791/// (`CHAR CHARACTER SET utf8mb4 BINARY`); MySQL's reversed spellings
792/// (`CHAR BINARY CHARACTER SET utf8mb4`, `CHAR BINARY ASCII`) fold onto this one shape,
793/// their exact written order recovered from the node span (an ADR-0011 spelling trade,
794/// mirroring the plural-interval and `SIGNED INTEGER` folds). The `CHARSET` synonym
795/// likewise folds to the canonical `CHARACTER SET`.
796#[derive(Clone, Debug, PartialEq, Eq, Hash)]
797#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
798#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
799pub struct CharsetAnnotation {
800    /// The character-set selector, or `None` when only the `BINARY` collation modifier was
801    /// written (`CHAR BINARY`, with no charset). At least one of `charset` / `binary` is
802    /// always present — an empty annotation is `None` on the [`DataType::Character`] node.
803    pub charset: Option<Charset>,
804    /// The charset name for [`Charset::Named`] (`CHARACTER SET <name>` / `CHARSET <name>`);
805    /// `None` for the keyword shortcuts ([`Charset::Ascii`]/[`Charset::Unicode`]/
806    /// [`Charset::Byte`]) and the bare-`BINARY` form. Held on this meta-bearing struct
807    /// rather than inside the [`Charset`] enum so the tag stays a plain `Copy` kind. The
808    /// name is a MySQL `ident_or_text`: a bare or backtick-quoted identifier, or a quoted
809    /// string (`CHARACTER SET 'utf8mb4'`), whose spelling round-trips from the [`Ident`]'s
810    /// quote style.
811    pub name: Option<Ident>,
812    /// Whether the `BINARY` binary-collation modifier accompanies the annotation
813    /// (`CHAR BINARY`, `CHAR CHARACTER SET x BINARY`, `CHAR ASCII BINARY`).
814    pub binary: bool,
815    /// Source location and node identity.
816    pub meta: Meta,
817}
818
819/// The character-set selector kind of a [`CharsetAnnotation`].
820#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
821#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
822#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
823pub enum Charset {
824    /// `CHARACTER SET <name>` or the `CHARSET <name>` synonym (folded to the canonical
825    /// `CHARACTER SET`). The name is carried by [`CharsetAnnotation::name`].
826    Named,
827    /// `ASCII` — MySQL shorthand for `CHARACTER SET latin1`.
828    Ascii,
829    /// `UNICODE` — MySQL shorthand for `CHARACTER SET ucs2`.
830    Unicode,
831    /// `BYTE` — MySQL/Oracle-compatibility shorthand producing a binary `CHAR`.
832    Byte,
833}
834
835#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
836#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
837#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
838/// The SQL binary type name forms represented by the AST.
839pub enum BinaryTypeName {
840    /// The fixed-length `BINARY` spelling.
841    Binary,
842    /// The variable-length `BINARY VARYING` spelling.
843    BinaryVarying,
844    /// The variable-length `VARBINARY` spelling.
845    Varbinary,
846    /// PostgreSQL's `BYTEA` spelling.
847    Bytea,
848}
849
850#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
851#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
852#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
853/// The SQL time type name forms represented by the AST.
854pub enum TimeTypeName {
855    /// The `TIME` spelling.
856    Time,
857    /// PostgreSQL's `TIMETZ` (`TIME WITH TIME ZONE`) spelling.
858    Timetz,
859}
860
861#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
862#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
863#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
864/// The SQL timestamp type name forms represented by the AST.
865pub enum TimestampTypeName {
866    /// The `TIMESTAMP` spelling.
867    Timestamp,
868    /// PostgreSQL's `TIMESTAMPTZ` (`TIMESTAMP WITH TIME ZONE`) spelling.
869    Timestamptz,
870    /// MySQL `DATETIME`: a timestamp-shaped type without time-zone semantics,
871    /// recognized only under
872    /// [`TypeNameSyntax::extended_scalar_type_names`](crate::dialect::TypeNameSyntax). It
873    /// reuses the timestamp variant (one canonical shape) and pins
874    /// [`TimeZone::Unspecified`](TimeZone), since `DATETIME` takes no zone suffix.
875    Datetime,
876}
877
878#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
879#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
880#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
881/// The SQL time zone forms represented by the AST.
882pub enum TimeZone {
883    /// No time-zone qualifier was written.
884    Unspecified,
885    /// `WITH TIME ZONE` — a time-zone-aware type.
886    WithTimeZone,
887    /// `WITHOUT TIME ZONE` — a time-zone-naive type.
888    WithoutTimeZone,
889}
890
891#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
892#[cfg_attr(feature = "serde-serialize", derive(serde::Serialize))]
893#[cfg_attr(feature = "serde-deserialize", derive(serde::Deserialize))]
894/// The SQL interval fields forms represented by the AST.
895pub enum IntervalFields {
896    /// `YEAR` — a year-precision interval qualifier.
897    Year,
898    /// `MONTH` — a month-precision interval qualifier.
899    Month,
900    /// `DAY` — a day-precision interval qualifier.
901    Day,
902    /// `HOUR` — an hour-precision interval qualifier.
903    Hour,
904    /// `MINUTE` — a minute-precision interval qualifier.
905    Minute,
906    /// `SECOND` — a second-precision interval qualifier.
907    Second,
908    /// `YEAR TO MONTH` — a year-and-month interval qualifier.
909    YearToMonth,
910    /// `DAY TO HOUR` — a day-to-hour interval qualifier.
911    DayToHour,
912    /// `DAY TO MINUTE` — a day-to-minute interval qualifier.
913    DayToMinute,
914    /// `DAY TO SECOND` — a day-to-second interval qualifier.
915    DayToSecond,
916    /// `HOUR TO MINUTE` — an hour-to-minute interval qualifier.
917    HourToMinute,
918    /// `HOUR TO SECOND` — an hour-to-second interval qualifier.
919    HourToSecond,
920    /// `MINUTE TO SECOND` — a minute-to-second interval qualifier.
921    MinuteToSecond,
922    // DuckDB-only extended units, admitted as `INTERVAL <amount> <unit>` multipliers
923    // (both singular and plural spellings) solely under
924    // [`ExpressionSyntax::relaxed_interval_syntax`](crate::dialect::ExpressionSyntax).
925    // ANSI/PostgreSQL never produce these — their interval grammar has no such
926    // qualifier — so the shared `DataType::Interval` render/accept path is unchanged.
927    // Each is a whole, simple qualifier: DuckDB has no `TO` composite for them and
928    // rejects a trailing precision, so (like the plural forms) they carry no precision.
929    /// `WEEK` — a DuckDB week interval qualifier (non-standard).
930    Week,
931    /// `QUARTER` — a DuckDB quarter interval qualifier (non-standard).
932    Quarter,
933    /// `DECADE` — a DuckDB decade interval qualifier (non-standard).
934    Decade,
935    /// `CENTURY` — a DuckDB century interval qualifier (non-standard).
936    Century,
937    /// `MILLENNIUM` — a DuckDB millennium interval qualifier (non-standard).
938    Millennium,
939    /// `MILLISECOND` — a DuckDB millisecond interval qualifier (non-standard).
940    Millisecond,
941    /// `MICROSECOND` — a DuckDB microsecond interval qualifier (non-standard).
942    Microsecond,
943    // MySQL-only microsecond composite units, admitted solely in the MySQL `interval`
944    // vocabulary (the `EVERY <expr> <unit>` event schedule and `INTERVAL <expr> <unit>`
945    // arithmetic). MySQL spells them with an underscore (`DAY_MICROSECOND`), not the ANSI
946    // `TO` composite; the standard `DataType::Interval` render never produces them (no
947    // dialect's INTERVAL type grammar admits a microsecond composite), so they extend the
948    // shared vocabulary without touching the ANSI interval type. The other MySQL composites
949    // (`DAY_HOUR`, `MINUTE_SECOND`, `YEAR_MONTH`, …) reuse the existing `*To*` variants.
950    /// `DAY_MICROSECOND` — a MySQL day-to-microsecond composite interval unit.
951    DayToMicrosecond,
952    /// `HOUR_MICROSECOND` — a MySQL hour-to-microsecond composite interval unit.
953    HourToMicrosecond,
954    /// `MINUTE_MICROSECOND` — a MySQL minute-to-microsecond composite interval unit.
955    MinuteToMicrosecond,
956    /// `SECOND_MICROSECOND` — a MySQL second-to-microsecond composite interval unit.
957    SecondToMicrosecond,
958}