Skip to main content

squonk_ast/render/
nodes.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Moderately AI Inc.
3
4//! Hand-written `Render` impls for the M1 node set.
5//!
6//! Parenthesization is *derived* from the one binding-power table,
7//! never stored: `Canonical`/`Redacted` add only the parens a looser-binding
8//! child would otherwise re-associate away, while `Parenthesized` wraps every
9//! binary/unary node and every nested set-operation operand so a precedence
10//! mis-bind shows up as different grouping.
11//!
12//! `Other(X)` arms recurse into the extension via `X: Render`. The `Render` bound is
13//! kept off the AST's `Extension` trait to avoid an AST -> renderer
14//! layering cycle, so the renderer adds it here at its own impl sites.
15
16use super::{Render, RenderCtx, RenderMode, RenderSpelling};
17use crate::ast::{
18    AccessControlStatement, AccountName, AfterMatchSkip, AggregateArgs, AliasSpelling,
19    AlterColumnAction, AlterColumnTarget, AlterDatabase, AlterDatabaseAction, AlterDatabaseOption,
20    AlterDatabaseOptions, AlterEvent, AlterExtension, AlterExtensionAction, AlterInstance,
21    AlterInstanceAction, AlterLogfileGroup, AlterObjectDepends, AlterObjectSchema,
22    AlterResourceGroup, AlterRoutine, AlterSequence, AlterSequenceOption, AlterServer, AlterSystem,
23    AlterSystemAction, AlterTable, AlterTableAction, AlterTablespace, AlterTablespaceAction,
24    AlterUser, AlterUserSpec, AlterView, AnalyzeHistogram, AnalyzeStatement, ApplyKind, ArgSyntax,
25    ArrayExpr, ArraySpelling, ArrayTypeSpelling, AsOfJoinKind, AssignGtidsKind, AttachStatement,
26    AuthOption, AutoIncrementSpelling, BinaryOperator, BinaryTypeName, BinlogStatement,
27    BitStringRadix, BitwiseXorSpelling, BlobTypeName, BooleanTypeName, CacheIndexKeyList,
28    CacheIndexKeyword, CacheIndexStatement, CacheIndexTable, CacheIndexTargets, CallStatement,
29    CaseExpr, CaseStatement, CastSyntax, CeilSpelling, ChangeReplicationSourceOption,
30    ChangeReplicationSourceOptionValue, CharacterSetKeyword, CharacterTypeName, Charset,
31    CharsetAnnotation, CharsetKeyword, CheckTableOption, CheckpointStatement, ChecksumTableOption,
32    CloneDataDirectory, CloneSsl, CloneStatement, CloseCursorStatement, ColumnConstraint,
33    ColumnDef, ColumnOption, ColumnsSpelling, CommentOnStatement, CommentTarget, CompoundStatement,
34    ComprehensionSource, ConditionInfoItem, ConditionInfoItemName, ConditionValue,
35    ConditionalBranch, ConfigParameter, ConflictAction, ConflictResolution, ConflictTarget,
36    ConstraintCharacteristics, ConstraintCheckTime, ConstraintsTarget, CopyDirection,
37    CopyIntoSource, CopyIntoStatement, CopyIntoTarget, CopyOption, CopyOptionValue, CopySource,
38    CopyStatement, CopyTarget, CreateDatabase, CreateEvent, CreateExtension, CreateExtensionOption,
39    CreateFunction, CreateIndex, CreateLogfileGroup, CreateMacro, CreateProcedure,
40    CreateResourceGroup, CreateSchema, CreateSecret, CreateSequence, CreateServer,
41    CreateSpatialReferenceSystem, CreateStoredTrigger, CreateTable, CreateTableBody,
42    CreateTableOption, CreateTableOptionKind, CreateTablespace, CreateTrigger, CreateType,
43    CreateTypeDefinition, CreateUser, CreateView, CreateVirtualTable, Cte, CteBody, CteCycleClause,
44    CteCycleMark, CteSearchClause, DataType, DatabaseKeyword, DeallocateKeyword,
45    DeallocateStatement, DecimalTypeName, Declaration, DefaultRoleTarget, DefaultValue, Definer,
46    Delete, DerivedSpelling, DescribeColumn, DescribeStatement, DetachPartitionMode,
47    DetachStatement, DiagnosticsArea, DiagnosticsInfo, DmlSelection, DmlTarget, DoArg,
48    DoExpressionsStatement, DoStatement, DoubleTypeName, DropBehavior, DropDatabase, DropEvent,
49    DropIndexOnTable, DropLogfileGroup, DropObjectKind, DropResourceGroup, DropSecretStmt,
50    DropServer, DropSpatialReferenceSystem, DropStatement, DropTablespace, DropTransform,
51    EmptyMatchesMode, EqualsSpelling, EventOnCompletion, EventSchedule, EventStatus,
52    ExcludeConstraint, ExcludeElement, ExcludeOperator, ExecuteStatement, ExecuteUsingStatement,
53    ExplainFormat, ExplainKeyword, ExplainOption, ExplainStatement, ExportStatement, Expr,
54    Extension, ExtensionVersion, ExtractExpr, FetchCursorStatement, FetchSpelling, FieldSelector,
55    FilterWhereSpelling, FlushOption, FlushStatement, FlushTablesLock, FlushTarget, ForClause,
56    ForJsonMode, ForRoot, ForXmlElements, ForXmlMode, ForceKind, ForeignKeyMatch, ForeignKeyRef,
57    FormatClause, FromFirstLast, FunctionArg, FunctionBody, FunctionCall, FunctionNullBehavior,
58    FunctionOption, FunctionParam, FunctionParamDefault, FunctionParamDefaultSpelling,
59    FunctionParamMode, GeneratedColumn, GeneratedColumnSpelling, GeneratedColumnStorage,
60    GetDiagnosticsStatement, GrantAs, GrantObject, Grantee, GroupByAllSpelling, GroupByItem,
61    GroupReplicationOption, HandlerAction, HandlerCondition, HandlerIndexDirection,
62    HandlerKeyComparison, HandlerOperation, HandlerReadSelector, HandlerScanDirection,
63    HandlerStatement, HelpStatement, HierarchicalClause, Ident, IdentityColumn, IdentityGeneration,
64    IdentityOption, IfStatement, ImportStatement, ImportTableStatement, IndexAlgorithm,
65    IndexColumn, IndexHint, IndexHintAction, IndexHintKeyword, IndexHintScope, IndexLock,
66    IndexLockAlgorithmOption, IndexedBy, Insert, InsertColumnMatching, InsertOverriding,
67    InsertSource, InsertTarget, InsertValue, InsertValues, InsertVerb, InstallComponentSetElement,
68    InstallComponentSetScope, InstallComponentSetValue, InstallStatement, InstanceLockStatement,
69    IntWidth, IntegerDivideSpelling, IntegerTypeName, IntervalFields, IntoTarget, IoThreadKeyword,
70    IsDistinctFromSpelling, IsJsonExpr, IsNotDistinctFromSpelling, IsolationLevel,
71    IterateStatement, Join, JoinConstraint, JoinOperator, JsonAggregateBody, JsonAggregateExpr,
72    JsonArrayBody, JsonArrayExpr, JsonBehavior, JsonBehaviorKind, JsonConstructorExpr,
73    JsonConstructorKind, JsonEncoding, JsonFormat, JsonFuncExpr, JsonFuncKind, JsonItemType,
74    JsonKeyValue, JsonKeyValueSpelling, JsonNullClause, JsonObjectExpr, JsonPassingArg,
75    JsonQuotesBehavior, JsonReturning, JsonTable, JsonTableColumn, JsonValueExpr,
76    JsonWrapperBehavior, KeyCacheName, KillStatement, KillTarget, LambdaParamSpelling,
77    LanguageName, LateralView, LeaveStatement, LikeSpelling, Limit, LimitBy, LimitPercent,
78    LimitSyntax, Literal, LiteralKind, LoadDataConcurrency, LoadDataDuplicate, LoadDataEnclosed,
79    LoadDataFieldOrVar, LoadDataFields, LoadDataFormat, LoadDataIgnoreRows, LoadDataIgnoreUnit,
80    LoadDataLines, LoadDataStatement, LoadFieldsSpelling, LoadIndexStatement, LoadIndexTable,
81    LoadIndexTargets, LoadStatement, LoadTarget, LockStrength, LockTablesStatement, LockWait,
82    LockingClause, LockingSpelling, LoopStatement, MacroBody, MacroParam, MacroSpelling,
83    MatchRecognize, MatchRecognizePattern, MatchSearchModifier, Measure, Merge, MergeAction,
84    MergeMatchKind, MergeWhenClause, ModuleArg, ModuloSpelling, NamedObjectKind,
85    NamedOperatorSpelling, NamedWindow, NoExt, NoWriteToBinlog, NormalizationForm, NotEqSpelling,
86    NullInclusion, NullTestSpelling, NullTreatment, ObjectName, ObjectRefKind, ObjectReference,
87    OnCommitAction, OnConflict, OnlySyntax, OpenCursorStatement, OpenJson, OpenJsonColumn,
88    OperatorArgs, OrderByAll, OrderByExpr, OrderByUsing, ParameterKind, ParameterSigil,
89    PartitionBound, PartitionElem, PartitionSelection, PartitionSpec, PartitionStrategy,
90    PasswordLockOption, PipeAggregateExpr, PipeOperator, PipeRenameItem, Pivot, PivotColumn,
91    PivotExpr, PivotSpelling, PivotValueSource, PragmaStatement, PrepareFromStatement,
92    PrepareSource, PrepareStatement, Privilege, PrivilegeKind, PrivilegeLevel,
93    PrivilegeLevelObject, PrivilegeObjectType, Privileges, PurgeStatement, PurgeTarget, Quantifier,
94    Query, QuoteStyle, ReadOnlyValue, ReferentialAction, RegexpSpelling, ReindexStatement,
95    RelationInheritance, RenameStatement, RepairTableOption, RepeatStatement, RepetitionQuantifier,
96    ReplicaSpelling, ReplicaThreadOption, ReplicaUntilCondition, ReplicationFilterRule,
97    ReplicationStatement, RequirePrimaryKeyCheck, ResourceGroupState, ResourceGroupThreadPriority,
98    ResourceGroupType, ResourceGroupVcpu, ResourceLimit, ReturnStatement, Returning, RewriteDbPair,
99    RoleSpec, RollupSpelling, RoutineKind, RoutineObjectKind, RoutineSignature, RowsFromItem,
100    RowsPerMatch, SampleClause, SampleUnit, SchemaObjectKind, SchemaRelocationObject, SecretOption,
101    SecretPersistence, Select, SelectDistinct, SelectItem, SelectSpelling, SemiAntiSide,
102    SemiStructuredAccessExpr, SemiStructuredPathSegment, ServerOption, ServerOptionKind,
103    SessionStatement, SessionVariableKind, SetAssignment, SetCharacterSetValue, SetExpr,
104    SetNamesValue, SetOperator, SetParameterValue, SetQuantifier, SetScope, SetValue,
105    SetVariableAssignment, SetVariableKeyword, SetVariableValue, Setting, ShowBare,
106    ShowColumnsSpelling, ShowCreateKind, ShowDiagnosticKind, ShowEngineArtifact, ShowFilter,
107    ShowFrom, ShowFromKeyword, ShowFunctionsFilter, ShowFunctionsScope, ShowIndexSpelling,
108    ShowLimit, ShowListing, ShowProfileType, ShowRef, ShowRefKind, ShowRefTarget, ShowRoutineKind,
109    ShowScope, ShowStatement, ShowTarget, SignalItem, SignalItemName, SignalStatement, Signedness,
110    SizeLiteral, SizeUnit, SpecialFunctionKeyword, SpecialSetValue, SqlDataAccess,
111    SqlSecurityContext, SrsAttribute, Statement, StatementInfoItem, StatementInfoItemName,
112    StringFunc, StructConstructorArg, StructConstructorField, StructField, StructKeySpelling,
113    StructTypeField, StructTypeSpelling, SubscriptKind, SubsetDefinition, SymbolDefinition,
114    SystemVariableScope, SystemVariableScopeKind, TableAlias, TableConstraint, TableConstraintDef,
115    TableElement, TableFactor, TableFunctionColumn, TableHint, TableKeyword, TableLikeAction,
116    TableLikeFeature, TableLikeOption, TableLock, TableLockKind, TableMaintenanceKind,
117    TableMaintenanceStatement, TableOption, TableOptionValue, TableRename, TableSample,
118    TableStorageParameter, TableVersion, TableWithJoins, TablespaceOption, TablespaceSizeOption,
119    TemporaryTableKind, TextTypeName, TimeTypeName, TimeZone, TimestampTypeName, TlsOption,
120    TlsRequirement, TransactionAccessMode, TransactionBlockKeyword, TransactionMode,
121    TransactionModeKind, TransactionStart, TransactionStatement, TriggerEvent, TriggerOrder,
122    TriggerTiming, TrimSide, TruthValue, UnaryOperator, UndoTablespaceState, UninstallStatement,
123    UnlockTablesStatement, Unpivot, UnpivotColumn, UnpivotSpelling, Update, UpdateAssignment,
124    UpdateExtensionsStatement, UpdateTupleSource, UpdateValue, Upsert, UseStatement, UserAttribute,
125    UserRename, UserRoleList, UserRoleListKind, UserSpec, VacuumAnalyze, VacuumStatement, Values,
126    ValuesItem, VcpuRange, ViewAlgorithm, ViewCheckOption, ViewOptions, WhileStatement,
127    WildcardOptions, WildcardRename, WildcardReplace, WindowDefinition, WindowFrame,
128    WindowFrameBound, WindowFrameExclusion, WindowFrameUnits, WindowSpec, With, WithRoleSpec,
129    WrappedTypeKind, XaAssociation, XaStartKeyword, XaStatement, XaSuspend, Xid, XmlAttribute,
130    XmlDocumentOrContent, XmlFunc, XmlIndentOption, XmlNamespace, XmlPassingMechanism,
131    XmlStandalone, XmlTable, XmlTableColumn, XmlWhitespaceOption,
132};
133use crate::dialect::TargetSpelling;
134use crate::precedence::{
135    BindingPower, BindingPowerTable, SetOperationBindingPowerTable, Side, UNPARENTHESIZED_IN_LIST,
136    needs_parens_between,
137};
138use std::fmt;
139use thin_vec::ThinVec;
140
141// The render-shape fingerprint pins (ADR-0013), one per AST source file
142// (`ast/<family>.rs`). Each `const _` compiles only while its `<0x…>` still equals
143// the matching `CURRENT_RENDER_SHAPE_<FAMILY>` hash sourcegen last wrote for that
144// file's slice of the AST shape. Change a shape in `ast/<family>.rs` (add, drop, or
145// retype a field or variant) and sourcegen regenerates a new
146// `RenderShapeFingerprint<0x…>` for THAT family only, so only that family's pin
147// below stops compiling with an `expected RenderShapeFingerprint<…>, found
148// RenderShapeFingerprint<…>` mismatch (the two const-generic fingerprints differ) —
149// the alarm that the hand-written impls below may no longer cover every field or
150// variant of that file's nodes. Do NOT just paste the new hash in to silence it:
151// that skips the audit this pin exists to force. Fix it in order (the procedure below):
152//   1. rerun `cargo run -p squonk-sourcegen` so `generated/render_skeleton.rs`
153//      reflects the new shape;
154//   2. audit the hand-written `Render` impls for that family's nodes (the impls are
155//      interleaved through this file — the const name, not a line range, is the
156//      index) against the regenerated `render_skeleton`, updating the render text;
157//   3. THEN copy the regenerated `CURRENT_RENDER_SHAPE_<FAMILY>` hash into the
158//      matching `<0x…>` below — leave every other family's pin untouched.
159// The compile error can only carry the two hashes, not this procedure, so it lives
160// here.
161//
162// Why per-family (ADR-0013 split): a shape change moves ONE pin line, so two agents
163// editing disjoint files (`expr.rs` vs `ddl.rs`) touch disjoint pins and merge with
164// no conflict, while two agents landing shapes in the SAME file still collide loudly
165// on that file's single shared pin. The pins and the `render_skeleton` module they
166// reference are all `#[cfg(test)]`-gated, so product builds don't compile the
167// thousands of dead skeleton lines; the drift alarm surfaces under `cargo nextest
168// run` (test builds), not `cargo build -p squonk-ast`.
169#[cfg(test)]
170use crate::generated::render_skeleton as skeleton;
171#[cfg(test)]
172const _: skeleton::RenderShapeFingerprint<0x668c6569aa20f965> = skeleton::CURRENT_RENDER_SHAPE_DCL;
173#[cfg(test)]
174const _: skeleton::RenderShapeFingerprint<0xea69d9b785020501> = skeleton::CURRENT_RENDER_SHAPE_DDL;
175#[cfg(test)]
176const _: skeleton::RenderShapeFingerprint<0xbc51c89e49732760> = skeleton::CURRENT_RENDER_SHAPE_DML;
177#[cfg(test)]
178const _: skeleton::RenderShapeFingerprint<0xbcc8a4346507a25c> = skeleton::CURRENT_RENDER_SHAPE_EXPR;
179#[cfg(test)]
180const _: skeleton::RenderShapeFingerprint<0xffd1225e92e4dab2> = skeleton::CURRENT_RENDER_SHAPE_EXT;
181#[cfg(test)]
182const _: skeleton::RenderShapeFingerprint<0xbc5427055a4e813c> =
183    skeleton::CURRENT_RENDER_SHAPE_IDENT;
184#[cfg(test)]
185const _: skeleton::RenderShapeFingerprint<0x509ea17dec9af986> =
186    skeleton::CURRENT_RENDER_SHAPE_LITERAL;
187#[cfg(test)]
188const _: skeleton::RenderShapeFingerprint<0x8b1ed704455adf27> =
189    skeleton::CURRENT_RENDER_SHAPE_MATCH_RECOGNIZE;
190#[cfg(test)]
191const _: skeleton::RenderShapeFingerprint<0x5ebfaa9fb0c1caf9> =
192    skeleton::CURRENT_RENDER_SHAPE_PIPE_OPS;
193#[cfg(test)]
194const _: skeleton::RenderShapeFingerprint<0xd3444362d3f03fe8> =
195    skeleton::CURRENT_RENDER_SHAPE_PIVOT;
196#[cfg(test)]
197const _: skeleton::RenderShapeFingerprint<0x305c97c17f4b1723> =
198    skeleton::CURRENT_RENDER_SHAPE_QUERY;
199#[cfg(test)]
200const _: skeleton::RenderShapeFingerprint<0x6f0ed6cfb3c83310> = skeleton::CURRENT_RENDER_SHAPE_STMT;
201#[cfg(test)]
202const _: skeleton::RenderShapeFingerprint<0xfb50d22f049b4cc7> =
203    skeleton::CURRENT_RENDER_SHAPE_STORED_PROGRAM;
204#[cfg(test)]
205const _: skeleton::RenderShapeFingerprint<0x0b580e5e0de8de03> = skeleton::CURRENT_RENDER_SHAPE_TCL;
206#[cfg(test)]
207const _: skeleton::RenderShapeFingerprint<0x03efd7c0644eb26a> = skeleton::CURRENT_RENDER_SHAPE_TY;
208#[cfg(test)]
209const _: skeleton::RenderShapeFingerprint<0x30c6b7a3fb94d049> = skeleton::CURRENT_RENDER_SHAPE_UTIL;
210#[cfg(test)]
211const _: skeleton::RenderShapeFingerprint<0x585b3bd313272236> =
212    skeleton::CURRENT_RENDER_SHAPE_WINDOW;
213
214// ---------------------------------------------------------------------------
215// Leaf vocabulary
216// ---------------------------------------------------------------------------
217
218impl Render for NoExt {
219    fn render(&self, _ctx: &RenderCtx<'_>, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        // `NoExt` is uninhabited: the stock AST has no extension node to render.
221        match *self {}
222    }
223}
224
225impl Render for Ident {
226    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        // Redacted rendering masks every identifier to a fixed placeholder so no
228        // original name reaches a query fingerprint or a PII-free log (ADR-0010),
229        // mirroring how `Literal` masks values to `?`. Emitting the mask before the
230        // symbol is resolved keeps the source spelling out of the output for every
231        // quote style and for a keyword used as an identifier; `ObjectName` renders
232        // one placeholder per dotted part, so qualified-name arity survives for
233        // query-shape discrimination while the names themselves do not.
234        if ctx.mode() == RenderMode::Redacted {
235            return f.write_str("id");
236        }
237        let text = ctx.resolve(self.sym);
238        // Doubling the close delimiter escapes an embedded copy, mirroring the
239        // lexer's doubled-close rule so the identifier round-trips. Bracket quoting
240        // is asymmetric, so only the close `]` doubles, never the open `[`.
241        let (open, close, doubled) = match self.quote {
242            QuoteStyle::None => return f.write_str(text),
243            // A `U&"…"` Unicode-escaped identifier's decoded value (`sym`) differs from its
244            // source spelling. Under a source-fidelity render the exact `U&"…" [UESCAPE 'c']`
245            // slice replays verbatim; a `TargetDialect` re-spell, the redacted fingerprint,
246            // or a detached node with no backing source fall through to the plain
247            // double-quoted decoded form — semantically identical, and the only spelling a
248            // non-`unicode_strings` target can carry.
249            QuoteStyle::UnicodeDouble => {
250                if honours_source_spelling(ctx) {
251                    if let Some(src) = ctx.slice(self.meta.span) {
252                        return f.write_str(src);
253                    }
254                }
255                ('"', '"', "\"\"")
256            }
257            QuoteStyle::Single => ('\'', '\'', "''"),
258            QuoteStyle::Double => ('"', '"', "\"\""),
259            QuoteStyle::Backtick => ('`', '`', "``"),
260            QuoteStyle::Bracket => ('[', ']', "]]"),
261        };
262        // The common identifier embeds no delimiter, so keep that path borrow-only
263        // and allocate the escaped copy only when one is actually present.
264        if text.contains(close) {
265            let escaped = text.replace(close, doubled);
266            write!(f, "{open}{escaped}{close}")
267        } else {
268            write!(f, "{open}{text}{close}")
269        }
270    }
271}
272
273impl Render for ObjectName {
274    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        for (i, part) in self.0.iter().enumerate() {
276            if i > 0 {
277                f.write_str(".")?;
278            }
279            part.render(ctx, f)?;
280        }
281        Ok(())
282    }
283}
284
285impl Render for Literal {
286    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        if ctx.mode() == RenderMode::Redacted {
288            return f.write_str("?");
289        }
290        if let Some(text) = ctx.slice(self.meta.span) {
291            // Exact source spelling round-trips (hex/scientific/quote style).
292            return f.write_str(text);
293        }
294        // A synthesized or detached literal has no backing source; fall back to a
295        // kind-based spelling so rendering stays total instead of panicking. The
296        // temporal forms re-emit a placeholder value of the right type, carrying the
297        // time-zone flag / interval qualifier the kind tag records.
298        match &self.kind {
299            LiteralKind::Null => f.write_str("NULL"),
300            LiteralKind::Boolean(true) => f.write_str("TRUE"),
301            LiteralKind::Boolean(false) => f.write_str("FALSE"),
302            LiteralKind::Integer | LiteralKind::Float | LiteralKind::Decimal => f.write_str("0"),
303            LiteralKind::String => f.write_str("''"),
304            LiteralKind::Date => f.write_str("DATE '1970-01-01'"),
305            LiteralKind::Time { time_zone } => {
306                f.write_str("TIME")?;
307                render_time_zone_suffix(*time_zone, f)?;
308                f.write_str(" '00:00:00'")
309            }
310            LiteralKind::Timestamp { time_zone } => {
311                f.write_str("TIMESTAMP")?;
312                render_time_zone_suffix(*time_zone, f)?;
313                f.write_str(" '1970-01-01 00:00:00'")
314            }
315            LiteralKind::Interval { fields, .. } => {
316                f.write_str("INTERVAL '0'")?;
317                match fields {
318                    Some(fields) => f.write_str(interval_fields_suffix(*fields)),
319                    None => Ok(()),
320                }
321            }
322            LiteralKind::BitString { radix } => f.write_str(match radix {
323                BitStringRadix::Binary => "B'0'",
324                BitStringRadix::Hex => "X'0'",
325            }),
326            LiteralKind::Money => f.write_str("$0"),
327        }
328    }
329}
330
331impl<X: Extension + Render> Render for DataType<X> {
332    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333        let target = type_render_target(ctx);
334        match self {
335            DataType::Boolean { spelling, .. } => f.write_str(boolean_type_name(target, *spelling)),
336            DataType::TinyInt { display_width, .. } => render_sized(f, "TINYINT", *display_width),
337            DataType::SmallInt { display_width, .. } => render_sized(f, "SMALLINT", *display_width),
338            DataType::MediumInt { display_width, .. } => {
339                render_sized(f, "MEDIUMINT", *display_width)
340            }
341            DataType::Integer {
342                spelling,
343                display_width,
344                ..
345            } => render_sized(f, integer_type_name(target, *spelling), *display_width),
346            DataType::BigInt { display_width, .. } => render_sized(f, "BIGINT", *display_width),
347            DataType::Decimal {
348                spelling,
349                precision,
350                scale,
351                ..
352            } => {
353                render_precision_scale(f, decimal_type_name(target, *spelling), *precision, *scale)
354            }
355            DataType::Float { precision, .. } => render_sized(f, "FLOAT", *precision),
356            DataType::Real { .. } => f.write_str("REAL"),
357            DataType::Double { spelling, .. } => f.write_str(double_type_name(*spelling)),
358            DataType::Text {
359                spelling, charset, ..
360            } => {
361                f.write_str(text_type_name(target, *spelling))?;
362                render_optional_charset_annotation(ctx, target, charset.as_deref(), f)
363            }
364            DataType::Blob { spelling, .. } => f.write_str(blob_type_name(target, *spelling)),
365            DataType::Character {
366                spelling,
367                size,
368                charset,
369                ..
370            } => {
371                render_sized(f, character_type_name(target, *spelling), *size)?;
372                render_optional_charset_annotation(ctx, target, charset.as_deref(), f)
373            }
374            DataType::Binary { spelling, size, .. } => {
375                render_sized(f, binary_type_name(target, *spelling), *size)
376            }
377            DataType::Bit { varying, size, .. } => {
378                render_sized(f, if *varying { "BIT VARYING" } else { "BIT" }, *size)
379            }
380            DataType::Json { .. } => f.write_str("JSON"),
381            DataType::Uuid { .. } => f.write_str("UUID"),
382            DataType::Date { .. } => f.write_str("DATE"),
383            DataType::Time {
384                spelling,
385                precision,
386                time_zone,
387                ..
388            } => render_time_type(target, *spelling, *precision, *time_zone, f),
389            DataType::Timestamp {
390                spelling,
391                precision,
392                time_zone,
393                ..
394            } => render_timestamp_type(target, *spelling, *precision, *time_zone, f),
395            DataType::Interval {
396                fields, precision, ..
397            } => render_interval_type(*fields, *precision, f),
398            DataType::Enum {
399                values, charset, ..
400            } => {
401                render_value_list_type(ctx, "ENUM", values, f)?;
402                render_optional_charset_annotation(ctx, target, charset.as_deref(), f)
403            }
404            DataType::Set {
405                values, charset, ..
406            } => {
407                render_value_list_type(ctx, "SET", values, f)?;
408                render_optional_charset_annotation(ctx, target, charset.as_deref(), f)
409            }
410            DataType::NumericModifier {
411                element,
412                signedness,
413                zerofill,
414                ..
415            } => render_numeric_modifier_type(ctx, element.as_deref(), *signedness, *zerofill, f),
416            DataType::Array {
417                element,
418                size,
419                spelling,
420                ..
421            } => render_array_type(ctx, element, *size, *spelling, f),
422            DataType::Struct {
423                fields, spelling, ..
424            } => match spelling {
425                StructTypeSpelling::AngleBracket => {
426                    render_composite_type_angle(ctx, "STRUCT", fields, f)
427                }
428                _ => render_composite_type(ctx, struct_type_keyword(*spelling), fields, f),
429            },
430            DataType::Union { members, .. } => render_composite_type(ctx, "UNION", members, f),
431            DataType::Map { key, value, .. } => {
432                f.write_str("MAP(")?;
433                key.render(ctx, f)?;
434                f.write_str(", ")?;
435                value.render(ctx, f)?;
436                f.write_str(")")
437            }
438            DataType::Wrapped { kind, inner, .. } => {
439                f.write_str(wrapped_type_keyword(*kind))?;
440                f.write_str("(")?;
441                inner.render(ctx, f)?;
442                f.write_str(")")
443            }
444            // ClickHouse's case-sensitive mixed-case spelling round-trips (`FixedString`,
445            // never `FIXEDSTRING`), the same canonical casing as `wrapped_type_keyword`.
446            DataType::FixedString { length, .. } => write!(f, "FixedString({length})"),
447            // ClickHouse's mixed-case spelling round-trips (`DateTime64`, never
448            // `DATETIME64`); the optional timezone re-emits its source-spelled string literal.
449            DataType::DateTime64 {
450                precision,
451                timezone,
452                ..
453            } => {
454                write!(f, "DateTime64({precision}")?;
455                if let Some(timezone) = timezone {
456                    f.write_str(", ")?;
457                    timezone.render(ctx, f)?;
458                }
459                f.write_str(")")
460            }
461            // ClickHouse's mixed-case spelling round-trips (`Nested`, never `NESTED`); the
462            // named-field list reuses the composite renderer, distinct from `STRUCT`/`UNION`
463            // only in its keyword.
464            DataType::Nested { fields, .. } => render_composite_type(ctx, "Nested", fields, f),
465            // ClickHouse's mixed-case bit-width spelling round-trips (`Int256`/`UInt256`,
466            // never `INT256`), the same canonical casing as the wrapper keywords.
467            DataType::FixedWidthInt { signed, width, .. } => {
468                f.write_str(fixed_width_int_name(*signed, *width))
469            }
470            DataType::UserDefined {
471                name, modifiers, ..
472            } => {
473                name.render(ctx, f)?;
474                render_literal_modifiers(ctx, f, modifiers)
475            }
476            // SQLite's liberal affinity name: its words re-rendered space-separated
477            // (preserving each word's source case and quote style), then the optional
478            // one-or-two-argument modifier list, so `LONG INTEGER` / `VARCHAR(123,456)`
479            // round-trip token-for-token.
480            DataType::Liberal { words, args, .. } => {
481                for (index, word) in words.iter().enumerate() {
482                    if index > 0 {
483                        f.write_str(" ")?;
484                    }
485                    word.render(ctx, f)?;
486                }
487                render_numeric_modifiers(f, args)
488            }
489            // The `Other(X)` seam delegates to the host node's own `Render`, exactly as
490            // `Expr::Other` / `Statement::Other` do; uninhabited under `NoExt`.
491            DataType::Other { ext, .. } => ext.render(ctx, f),
492        }
493    }
494}
495
496impl<X: Extension + Render> Render for StructTypeField<X> {
497    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
498        self.name.render(ctx, f)?;
499        f.write_str(" ")?;
500        self.ty.render(ctx, f)
501    }
502}
503
504/// The canonical type-name spelling family a `DataType` render targets.
505///
506/// Derived from the render [`RenderSpelling`] mode and — for a target-dialect render —
507/// the target [`FeatureSet`](crate::dialect::FeatureSet)'s
508/// [`TargetSpelling`] data, so the renderer reads the
509/// PostgreSQL-vs-ANSI choice from a field rather than recognizing a preset by identity
510/// (no `postgres` feature gate). `PreserveSource` keeps the AST's own syntax tag; the
511/// dialect families select the per-construct spelling tables below.
512#[derive(Clone, Copy, Debug, PartialEq, Eq)]
513enum TypeRenderTarget {
514    PreserveSource,
515    Ansi,
516    Postgres,
517}
518
519fn type_render_target(ctx: &RenderCtx<'_>) -> TypeRenderTarget {
520    // The target's `TargetSpelling` is the data-driven contract for which canonical
521    // spellings a target-dialect render emits; `PreserveSource` ignores the target and
522    // keeps the AST's own syntax tag (ADR-0011).
523    match ctx.spelling() {
524        RenderSpelling::PreserveSource => TypeRenderTarget::PreserveSource,
525        RenderSpelling::TargetDialect => match ctx.target().target_spelling {
526            TargetSpelling::Ansi => TypeRenderTarget::Ansi,
527            TargetSpelling::Postgres => TypeRenderTarget::Postgres,
528        },
529    }
530}
531
532fn boolean_type_name(target: TypeRenderTarget, spelling: BooleanTypeName) -> &'static str {
533    match target {
534        TypeRenderTarget::PreserveSource => match spelling {
535            BooleanTypeName::Boolean => "BOOLEAN",
536            BooleanTypeName::Bool => "BOOL",
537        },
538        TypeRenderTarget::Ansi => "BOOLEAN",
539        TypeRenderTarget::Postgres => "BOOLEAN",
540    }
541}
542
543fn integer_type_name(target: TypeRenderTarget, spelling: IntegerTypeName) -> &'static str {
544    match target {
545        TypeRenderTarget::PreserveSource => match spelling {
546            IntegerTypeName::Int => "INT",
547            IntegerTypeName::Integer => "INTEGER",
548        },
549        TypeRenderTarget::Ansi => "INTEGER",
550        TypeRenderTarget::Postgres => "INTEGER",
551    }
552}
553
554fn decimal_type_name(target: TypeRenderTarget, spelling: DecimalTypeName) -> &'static str {
555    match target {
556        TypeRenderTarget::PreserveSource => match spelling {
557            DecimalTypeName::Decimal => "DECIMAL",
558            DecimalTypeName::Dec => "DEC",
559            DecimalTypeName::Numeric => "NUMERIC",
560        },
561        TypeRenderTarget::Ansi => "DECIMAL",
562        TypeRenderTarget::Postgres => "NUMERIC",
563    }
564}
565
566fn double_type_name(spelling: DoubleTypeName) -> &'static str {
567    match spelling {
568        DoubleTypeName::DoublePrecision => "DOUBLE PRECISION",
569        DoubleTypeName::Double => "DOUBLE",
570    }
571}
572
573fn text_type_name(target: TypeRenderTarget, spelling: TextTypeName) -> &'static str {
574    match target {
575        TypeRenderTarget::PreserveSource => match spelling {
576            TextTypeName::Text => "TEXT",
577            TextTypeName::TinyText => "TINYTEXT",
578            TextTypeName::MediumText => "MEDIUMTEXT",
579            TextTypeName::LongText => "LONGTEXT",
580        },
581        // The MySQL size family has no standard spelling; collapse it to the portable
582        // `TEXT` when targeting another dialect.
583        TypeRenderTarget::Ansi | TypeRenderTarget::Postgres => "TEXT",
584    }
585}
586
587fn blob_type_name(target: TypeRenderTarget, spelling: BlobTypeName) -> &'static str {
588    match target {
589        TypeRenderTarget::PreserveSource => match spelling {
590            BlobTypeName::Blob => "BLOB",
591            BlobTypeName::TinyBlob => "TINYBLOB",
592            BlobTypeName::MediumBlob => "MEDIUMBLOB",
593            BlobTypeName::LongBlob => "LONGBLOB",
594        },
595        // No standard binary-LOB spelling; keep `BLOB` for ANSI and map to the
596        // PostgreSQL binary type for a PostgreSQL target.
597        TypeRenderTarget::Ansi => "BLOB",
598        TypeRenderTarget::Postgres => "BYTEA",
599    }
600}
601
602fn character_type_name(target: TypeRenderTarget, spelling: CharacterTypeName) -> &'static str {
603    // Whether this spelling is a `VARYING` (variable-length) character type, used
604    // to map the national-character spellings onto a target's varying/fixed form.
605    let varying = matches!(
606        spelling,
607        CharacterTypeName::CharVarying
608            | CharacterTypeName::CharacterVarying
609            | CharacterTypeName::Varchar
610            | CharacterTypeName::NcharVarying
611            | CharacterTypeName::NationalCharVarying
612            | CharacterTypeName::NationalCharacterVarying
613    );
614    match target {
615        TypeRenderTarget::PreserveSource => match spelling {
616            CharacterTypeName::Char => "CHAR",
617            CharacterTypeName::Character => "CHARACTER",
618            CharacterTypeName::CharVarying => "CHAR VARYING",
619            CharacterTypeName::CharacterVarying => "CHARACTER VARYING",
620            CharacterTypeName::Varchar => "VARCHAR",
621            CharacterTypeName::Nchar => "NCHAR",
622            CharacterTypeName::NcharVarying => "NCHAR VARYING",
623            CharacterTypeName::NationalChar => "NATIONAL CHAR",
624            CharacterTypeName::NationalCharVarying => "NATIONAL CHAR VARYING",
625            CharacterTypeName::NationalCharacter => "NATIONAL CHARACTER",
626            CharacterTypeName::NationalCharacterVarying => "NATIONAL CHARACTER VARYING",
627        },
628        TypeRenderTarget::Ansi => {
629            if varying {
630                "CHARACTER VARYING"
631            } else {
632                "CHARACTER"
633            }
634        }
635        TypeRenderTarget::Postgres => {
636            if varying {
637                "VARCHAR"
638            } else {
639                "CHAR"
640            }
641        }
642    }
643}
644
645/// Render a MySQL character-set type annotation ([`CharsetAnnotation`]) in its canonical
646/// order — the charset selector first, then `BINARY` — with a leading space to follow the
647/// type. MySQL's reversed spellings (`BINARY CHARACTER SET x`, `BINARY ASCII`) and the
648/// `CHARSET` synonym fold onto this canonical form (an ADR-0011 spelling trade; the exact
649/// written order stays recoverable from the node span).
650///
651/// The annotation has no portable spelling, so it renders only for a source-preserving
652/// round-trip; a target-dialect render drops it (like the TEXT-family size collapse).
653/// `PreserveSource` is the only target that can carry one — the field is `None` for every
654/// non-MySQL dialect.
655fn render_optional_charset_annotation(
656    ctx: &RenderCtx<'_>,
657    target: TypeRenderTarget,
658    annotation: Option<&CharsetAnnotation>,
659    f: &mut fmt::Formatter<'_>,
660) -> fmt::Result {
661    let Some(annotation) = annotation else {
662        return Ok(());
663    };
664    if target != TypeRenderTarget::PreserveSource {
665        return Ok(());
666    }
667    match annotation.charset {
668        Some(Charset::Named) => {
669            f.write_str(" CHARACTER SET ")?;
670            // `name` is `Some` for the `Named` selector (parser invariant); a detached node
671            // with a missing name renders the bare keyword rather than panicking.
672            if let Some(name) = &annotation.name {
673                name.render(ctx, f)?;
674            }
675        }
676        Some(Charset::Ascii) => f.write_str(" ASCII")?,
677        Some(Charset::Unicode) => f.write_str(" UNICODE")?,
678        Some(Charset::Byte) => f.write_str(" BYTE")?,
679        None => {}
680    }
681    if annotation.binary {
682        f.write_str(" BINARY")?;
683    }
684    Ok(())
685}
686
687fn binary_type_name(target: TypeRenderTarget, spelling: BinaryTypeName) -> &'static str {
688    match target {
689        TypeRenderTarget::PreserveSource => match spelling {
690            BinaryTypeName::Binary => "BINARY",
691            BinaryTypeName::BinaryVarying => "BINARY VARYING",
692            BinaryTypeName::Varbinary => "VARBINARY",
693            BinaryTypeName::Bytea => "BYTEA",
694        },
695        TypeRenderTarget::Ansi => match spelling {
696            BinaryTypeName::Binary => "BINARY",
697            BinaryTypeName::BinaryVarying => "BINARY VARYING",
698            BinaryTypeName::Varbinary => "BINARY VARYING",
699            BinaryTypeName::Bytea => "BYTEA",
700        },
701        TypeRenderTarget::Postgres => match spelling {
702            BinaryTypeName::Binary => "BYTEA",
703            BinaryTypeName::BinaryVarying => "BYTEA",
704            BinaryTypeName::Varbinary => "BYTEA",
705            BinaryTypeName::Bytea => "BYTEA",
706        },
707    }
708}
709
710fn render_time_type(
711    target: TypeRenderTarget,
712    spelling: TimeTypeName,
713    precision: Option<u32>,
714    time_zone: TimeZone,
715    f: &mut fmt::Formatter<'_>,
716) -> fmt::Result {
717    match target {
718        TypeRenderTarget::PreserveSource => match spelling {
719            TimeTypeName::Time => {
720                render_sized(f, "TIME", precision)?;
721                render_time_zone_suffix(time_zone, f)
722            }
723            TimeTypeName::Timetz => render_sized(f, "TIMETZ", precision),
724        },
725        TypeRenderTarget::Ansi => {
726            render_sized(f, "TIME", precision)?;
727            render_time_zone_suffix(time_zone_from_spelling(time_zone, spelling), f)
728        }
729        TypeRenderTarget::Postgres => match time_zone_from_spelling(time_zone, spelling) {
730            TimeZone::Unspecified => render_sized(f, "TIME", precision),
731            TimeZone::WithTimeZone => render_sized(f, "TIMETZ", precision),
732            TimeZone::WithoutTimeZone => {
733                render_sized(f, "TIME", precision)?;
734                f.write_str(" WITHOUT TIME ZONE")
735            }
736        },
737    }
738}
739
740fn render_timestamp_type(
741    target: TypeRenderTarget,
742    spelling: TimestampTypeName,
743    precision: Option<u32>,
744    time_zone: TimeZone,
745    f: &mut fmt::Formatter<'_>,
746) -> fmt::Result {
747    match target {
748        TypeRenderTarget::PreserveSource => match spelling {
749            TimestampTypeName::Timestamp => {
750                render_sized(f, "TIMESTAMP", precision)?;
751                render_time_zone_suffix(time_zone, f)
752            }
753            TimestampTypeName::Timestamptz => render_sized(f, "TIMESTAMPTZ", precision),
754            TimestampTypeName::Datetime => render_sized(f, "DATETIME", precision),
755        },
756        TypeRenderTarget::Ansi => {
757            render_sized(f, "TIMESTAMP", precision)?;
758            render_time_zone_suffix(time_zone_from_timestamp_spelling(time_zone, spelling), f)
759        }
760        TypeRenderTarget::Postgres => {
761            match time_zone_from_timestamp_spelling(time_zone, spelling) {
762                TimeZone::Unspecified => render_sized(f, "TIMESTAMP", precision),
763                TimeZone::WithTimeZone => render_sized(f, "TIMESTAMPTZ", precision),
764                TimeZone::WithoutTimeZone => {
765                    render_sized(f, "TIMESTAMP", precision)?;
766                    f.write_str(" WITHOUT TIME ZONE")
767                }
768            }
769        }
770    }
771}
772
773fn time_zone_from_spelling(time_zone: TimeZone, spelling: TimeTypeName) -> TimeZone {
774    match spelling {
775        TimeTypeName::Time => time_zone,
776        TimeTypeName::Timetz => TimeZone::WithTimeZone,
777    }
778}
779
780fn time_zone_from_timestamp_spelling(time_zone: TimeZone, spelling: TimestampTypeName) -> TimeZone {
781    match spelling {
782        TimestampTypeName::Timestamp => time_zone,
783        TimestampTypeName::Timestamptz => TimeZone::WithTimeZone,
784        // MySQL `DATETIME` carries no zone, so a non-PreserveSource target renders it
785        // as a plain `TIMESTAMP` (its closest portable form).
786        TimestampTypeName::Datetime => TimeZone::Unspecified,
787    }
788}
789
790fn render_time_zone_suffix(time_zone: TimeZone, f: &mut fmt::Formatter<'_>) -> fmt::Result {
791    match time_zone {
792        TimeZone::Unspecified => Ok(()),
793        TimeZone::WithTimeZone => f.write_str(" WITH TIME ZONE"),
794        TimeZone::WithoutTimeZone => f.write_str(" WITHOUT TIME ZONE"),
795    }
796}
797
798fn render_interval_type(
799    fields: Option<IntervalFields>,
800    precision: Option<u32>,
801    f: &mut fmt::Formatter<'_>,
802) -> fmt::Result {
803    f.write_str("INTERVAL")?;
804    match fields {
805        Some(IntervalFields::Year) => render_interval_field(f, "YEAR", precision),
806        Some(IntervalFields::Month) => render_interval_field(f, "MONTH", precision),
807        Some(IntervalFields::Day) => render_interval_field(f, "DAY", precision),
808        Some(IntervalFields::Hour) => render_interval_field(f, "HOUR", precision),
809        Some(IntervalFields::Minute) => render_interval_field(f, "MINUTE", precision),
810        Some(IntervalFields::Second) => render_interval_field(f, "SECOND", precision),
811        Some(IntervalFields::YearToMonth) => f.write_str(" YEAR TO MONTH"),
812        Some(IntervalFields::DayToHour) => f.write_str(" DAY TO HOUR"),
813        Some(IntervalFields::DayToMinute) => f.write_str(" DAY TO MINUTE"),
814        Some(IntervalFields::DayToSecond) => {
815            f.write_str(" DAY TO ")?;
816            render_sized(f, "SECOND", precision)
817        }
818        Some(IntervalFields::HourToMinute) => f.write_str(" HOUR TO MINUTE"),
819        Some(IntervalFields::HourToSecond) => {
820            f.write_str(" HOUR TO ")?;
821            render_sized(f, "SECOND", precision)
822        }
823        Some(IntervalFields::MinuteToSecond) => {
824            f.write_str(" MINUTE TO ")?;
825            render_sized(f, "SECOND", precision)
826        }
827        Some(IntervalFields::Week) => render_interval_field(f, "WEEK", precision),
828        Some(IntervalFields::Quarter) => render_interval_field(f, "QUARTER", precision),
829        Some(IntervalFields::Decade) => render_interval_field(f, "DECADE", precision),
830        Some(IntervalFields::Century) => render_interval_field(f, "CENTURY", precision),
831        Some(IntervalFields::Millennium) => render_interval_field(f, "MILLENNIUM", precision),
832        Some(IntervalFields::Millisecond) => render_interval_field(f, "MILLISECOND", precision),
833        Some(IntervalFields::Microsecond) => render_interval_field(f, "MICROSECOND", precision),
834        // MySQL-only microsecond composites: no ANSI INTERVAL-type spelling exists (they are
835        // produced only by the MySQL `interval` vocabulary, rendered in underscore form by
836        // `render_mysql_interval_unit`), so the ANSI type render uses the descriptive `TO`
837        // form for exhaustiveness — no dialect's interval-type grammar reaches these arms.
838        Some(IntervalFields::DayToMicrosecond) => {
839            render_interval_field(f, "DAY TO MICROSECOND", precision)
840        }
841        Some(IntervalFields::HourToMicrosecond) => {
842            render_interval_field(f, "HOUR TO MICROSECOND", precision)
843        }
844        Some(IntervalFields::MinuteToMicrosecond) => {
845            render_interval_field(f, "MINUTE TO MICROSECOND", precision)
846        }
847        Some(IntervalFields::SecondToMicrosecond) => {
848            render_interval_field(f, "SECOND TO MICROSECOND", precision)
849        }
850        None => match precision {
851            Some(precision) => write!(f, "({precision})"),
852            None => Ok(()),
853        },
854    }
855}
856
857fn render_interval_field(
858    f: &mut fmt::Formatter<'_>,
859    name: &str,
860    precision: Option<u32>,
861) -> fmt::Result {
862    f.write_str(" ")?;
863    render_sized(f, name, precision)
864}
865
866/// The trailing field qualifier of an interval, as a leading-space suffix
867/// (` DAY`, ` YEAR TO MONTH`, …). Used by the synthetic-literal fallback, which has
868/// no source precision to place, so this omits any `(precision)`.
869fn interval_fields_suffix(fields: IntervalFields) -> &'static str {
870    match fields {
871        IntervalFields::Year => " YEAR",
872        IntervalFields::Month => " MONTH",
873        IntervalFields::Day => " DAY",
874        IntervalFields::Hour => " HOUR",
875        IntervalFields::Minute => " MINUTE",
876        IntervalFields::Second => " SECOND",
877        IntervalFields::YearToMonth => " YEAR TO MONTH",
878        IntervalFields::DayToHour => " DAY TO HOUR",
879        IntervalFields::DayToMinute => " DAY TO MINUTE",
880        IntervalFields::DayToSecond => " DAY TO SECOND",
881        IntervalFields::HourToMinute => " HOUR TO MINUTE",
882        IntervalFields::HourToSecond => " HOUR TO SECOND",
883        IntervalFields::MinuteToSecond => " MINUTE TO SECOND",
884        IntervalFields::Week => " WEEK",
885        IntervalFields::Quarter => " QUARTER",
886        IntervalFields::Decade => " DECADE",
887        IntervalFields::Century => " CENTURY",
888        IntervalFields::Millennium => " MILLENNIUM",
889        IntervalFields::Millisecond => " MILLISECOND",
890        IntervalFields::Microsecond => " MICROSECOND",
891        IntervalFields::DayToMicrosecond => " DAY TO MICROSECOND",
892        IntervalFields::HourToMicrosecond => " HOUR TO MICROSECOND",
893        IntervalFields::MinuteToMicrosecond => " MINUTE TO MICROSECOND",
894        IntervalFields::SecondToMicrosecond => " SECOND TO MICROSECOND",
895    }
896}
897
898/// The MySQL `interval` unit vocabulary — one keyword per [`IntervalFields`], in MySQL's
899/// underscore spelling (`DAY_HOUR`, `MINUTE_SECOND`, `YEAR_MONTH`), as a leading-space
900/// suffix. This is the render counterpart of the parser's MySQL interval-unit reader: the
901/// MySQL `EVERY <expr> <unit>` event schedule reuses the shared [`IntervalFields`]
902/// vocabulary but spells its composites with an underscore, never the ANSI `TO` form. The
903/// DuckDB-only extended units (`DECADE`/`CENTURY`/… and `MILLISECOND`) are unreachable here:
904/// MySQL's `interval` production admits only the units below, so the parser never yields
905/// them for a MySQL schedule.
906fn mysql_interval_unit_suffix(unit: IntervalFields) -> &'static str {
907    match unit {
908        IntervalFields::Year => " YEAR",
909        IntervalFields::Month => " MONTH",
910        IntervalFields::Day => " DAY",
911        IntervalFields::Hour => " HOUR",
912        IntervalFields::Minute => " MINUTE",
913        IntervalFields::Second => " SECOND",
914        IntervalFields::Week => " WEEK",
915        IntervalFields::Quarter => " QUARTER",
916        IntervalFields::Microsecond => " MICROSECOND",
917        IntervalFields::YearToMonth => " YEAR_MONTH",
918        IntervalFields::DayToHour => " DAY_HOUR",
919        IntervalFields::DayToMinute => " DAY_MINUTE",
920        IntervalFields::DayToSecond => " DAY_SECOND",
921        IntervalFields::HourToMinute => " HOUR_MINUTE",
922        IntervalFields::HourToSecond => " HOUR_SECOND",
923        IntervalFields::MinuteToSecond => " MINUTE_SECOND",
924        IntervalFields::DayToMicrosecond => " DAY_MICROSECOND",
925        IntervalFields::HourToMicrosecond => " HOUR_MICROSECOND",
926        IntervalFields::MinuteToMicrosecond => " MINUTE_MICROSECOND",
927        IntervalFields::SecondToMicrosecond => " SECOND_MICROSECOND",
928        // No MySQL `interval` keyword; the parser never yields these for a MySQL schedule.
929        IntervalFields::Decade
930        | IntervalFields::Century
931        | IntervalFields::Millennium
932        | IntervalFields::Millisecond => " MICROSECOND",
933    }
934}
935
936fn render_sized(f: &mut fmt::Formatter<'_>, name: &str, size: Option<u32>) -> fmt::Result {
937    match size {
938        Some(size) => write!(f, "{name}({size})"),
939        None => f.write_str(name),
940    }
941}
942
943fn render_precision_scale(
944    f: &mut fmt::Formatter<'_>,
945    name: &str,
946    precision: Option<i32>,
947    scale: Option<i32>,
948) -> fmt::Result {
949    f.write_str(name)?;
950    match (precision, scale) {
951        (Some(precision), Some(scale)) => write!(f, "({precision}, {scale})"),
952        (Some(precision), None) => write!(f, "({precision})"),
953        (None, Some(scale)) => write!(f, "(*, {scale})"),
954        (None, None) => Ok(()),
955    }
956}
957
958/// Render a user-defined type's constant modifier list — `(3)`, `(10, 2)`, or the DuckDB
959/// string form `('OGC:CRS84')` — each modifier by its exact source spelling. Empty list
960/// renders nothing.
961///
962/// A type modifier is part of the *type*, not a value literal (`DECIMAL(10, 2)` and
963/// `DECIMAL(5, 3)` are different types), so it is rendered verbatim even in
964/// [`RenderMode::Redacted`] — matching the built-in numeric-modifier path
965/// ([`render_numeric_modifiers`]) and never masking to `?`. The exact span text
966/// round-trips the integer/string spelling; a detached node with no backing source falls
967/// back to the literal's kind-based spelling for totality.
968fn render_literal_modifiers(
969    ctx: &RenderCtx<'_>,
970    f: &mut fmt::Formatter<'_>,
971    modifiers: &[Literal],
972) -> fmt::Result {
973    if modifiers.is_empty() {
974        return Ok(());
975    }
976    f.write_str("(")?;
977    for (index, modifier) in modifiers.iter().enumerate() {
978        if index > 0 {
979            f.write_str(", ")?;
980        }
981        if let Some(text) = ctx.slice(modifier.meta.span) {
982            f.write_str(text)?;
983        } else {
984            modifier.render(ctx, f)?;
985        }
986    }
987    f.write_str(")")
988}
989
990fn render_numeric_modifiers(f: &mut fmt::Formatter<'_>, modifiers: &[u32]) -> fmt::Result {
991    if modifiers.is_empty() {
992        return Ok(());
993    }
994    f.write_str("(")?;
995    for (index, modifier) in modifiers.iter().enumerate() {
996        if index > 0 {
997            f.write_str(", ")?;
998        }
999        write!(f, "{modifier}")?;
1000    }
1001    f.write_str(")")
1002}
1003
1004/// Render a MySQL `ENUM(...)` / `SET(...)` value-list type. Each member renders as
1005/// its source-spelled string constant.
1006fn render_value_list_type(
1007    ctx: &RenderCtx<'_>,
1008    name: &str,
1009    values: &[Literal],
1010    f: &mut fmt::Formatter<'_>,
1011) -> fmt::Result {
1012    f.write_str(name)?;
1013    f.write_str("(")?;
1014    render_comma_separated(values, ctx, f)?;
1015    f.write_str(")")
1016}
1017
1018/// The keyword an anonymous composite type ([`DataType::Struct`]) renders under.
1019/// The ClickHouse keyword a [`DataType::Wrapped`] type combinator renders under. The
1020/// canonical mixed-case spelling round-trips ClickHouse's case-sensitive type name
1021/// (`Nullable`, never `NULLABLE`), even though the case-insensitive keyword parser
1022/// accepts any casing on input.
1023fn wrapped_type_keyword(kind: WrappedTypeKind) -> &'static str {
1024    match kind {
1025        WrappedTypeKind::Nullable => "Nullable",
1026        WrappedTypeKind::LowCardinality => "LowCardinality",
1027    }
1028}
1029
1030/// The canonical spelling of a ClickHouse [`DataType::FixedWidthInt`] type name, e.g.
1031/// `(true, IntWidth::W256)` → `Int256`, `(false, IntWidth::W8)` → `UInt8`. Mixed-case
1032/// round-trips ClickHouse's case-sensitive type name (`Int256`, never `INT256`), even
1033/// though the case-insensitive keyword parser accepts any casing on input.
1034fn fixed_width_int_name(signed: bool, width: IntWidth) -> &'static str {
1035    match (signed, width) {
1036        (true, IntWidth::W8) => "Int8",
1037        (true, IntWidth::W16) => "Int16",
1038        (true, IntWidth::W32) => "Int32",
1039        (true, IntWidth::W64) => "Int64",
1040        (true, IntWidth::W128) => "Int128",
1041        (true, IntWidth::W256) => "Int256",
1042        (false, IntWidth::W8) => "UInt8",
1043        (false, IntWidth::W16) => "UInt16",
1044        (false, IntWidth::W32) => "UInt32",
1045        (false, IntWidth::W64) => "UInt64",
1046        (false, IntWidth::W128) => "UInt128",
1047        (false, IntWidth::W256) => "UInt256",
1048    }
1049}
1050
1051fn struct_type_keyword(spelling: StructTypeSpelling) -> &'static str {
1052    match spelling {
1053        StructTypeSpelling::Struct | StructTypeSpelling::AngleBracket => "STRUCT",
1054        StructTypeSpelling::Row => "ROW",
1055    }
1056}
1057
1058/// BigQuery `STRUCT<field TYPE, …>` / angle-bracket composite list.
1059fn render_composite_type_angle<X: Extension + Render>(
1060    ctx: &RenderCtx<'_>,
1061    keyword: &str,
1062    fields: &[StructTypeField<X>],
1063    f: &mut fmt::Formatter<'_>,
1064) -> fmt::Result {
1065    f.write_str(keyword)?;
1066    f.write_str("<")?;
1067    render_comma_separated(fields, ctx, f)?;
1068    f.write_str(">")
1069}
1070
1071/// Render an anonymous composite type: `<keyword>(name TYPE, ...)`, shared by
1072/// [`DataType::Struct`] (`STRUCT`/`ROW`), [`DataType::Union`], and the ClickHouse
1073/// [`DataType::Nested`].
1074fn render_composite_type<X: Extension + Render>(
1075    ctx: &RenderCtx<'_>,
1076    keyword: &str,
1077    fields: &[StructTypeField<X>],
1078    f: &mut fmt::Formatter<'_>,
1079) -> fmt::Result {
1080    f.write_str(keyword)?;
1081    f.write_str("(")?;
1082    render_comma_separated(fields, ctx, f)?;
1083    f.write_str(")")
1084}
1085
1086/// Render an array-type suffix per its written surface: bracket `T[]`/`T[n]` or keyword
1087/// `T ARRAY`/`T ARRAY[n]`, with the fixed-size bound when present.
1088fn render_array_type<X: Extension + Render>(
1089    ctx: &RenderCtx<'_>,
1090    element: &DataType<X>,
1091    size: Option<u32>,
1092    spelling: ArrayTypeSpelling,
1093    f: &mut fmt::Formatter<'_>,
1094) -> fmt::Result {
1095    if matches!(spelling, ArrayTypeSpelling::AngleBracket) {
1096        f.write_str("ARRAY<")?;
1097        element.render(ctx, f)?;
1098        return f.write_str(">");
1099    }
1100    element.render(ctx, f)?;
1101    match spelling {
1102        ArrayTypeSpelling::Bracket => match size {
1103            Some(n) => write!(f, "[{n}]"),
1104            None => f.write_str("[]"),
1105        },
1106        ArrayTypeSpelling::Keyword => {
1107            f.write_str(" ARRAY")?;
1108            match size {
1109                Some(n) => write!(f, "[{n}]"),
1110                None => Ok(()),
1111            }
1112        }
1113        ArrayTypeSpelling::AngleBracket => unreachable!("handled above"),
1114    }
1115}
1116
1117/// Render a MySQL numeric type with its `SIGNED`/`UNSIGNED`/`ZEROFILL` modifiers,
1118/// space-separating only the parts that are present. A `None` element is the
1119/// standalone integer cast target (`CAST(x AS UNSIGNED)`).
1120fn render_numeric_modifier_type<X: Extension + Render>(
1121    ctx: &RenderCtx<'_>,
1122    element: Option<&DataType<X>>,
1123    signedness: Signedness,
1124    zerofill: bool,
1125    f: &mut fmt::Formatter<'_>,
1126) -> fmt::Result {
1127    let mut wrote = false;
1128    if let Some(element) = element {
1129        element.render(ctx, f)?;
1130        wrote = true;
1131    }
1132    let sign = match signedness {
1133        Signedness::Unspecified => None,
1134        Signedness::Signed => Some("SIGNED"),
1135        Signedness::Unsigned => Some("UNSIGNED"),
1136    };
1137    if let Some(sign) = sign {
1138        if wrote {
1139            f.write_str(" ")?;
1140        }
1141        f.write_str(sign)?;
1142        wrote = true;
1143    }
1144    if zerofill {
1145        if wrote {
1146            f.write_str(" ")?;
1147        }
1148        f.write_str("ZEROFILL")?;
1149    }
1150    Ok(())
1151}
1152
1153impl Render for BinaryOperator {
1154    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1155        // The surrounding spaces are added by the parent expression so the token
1156        // itself stays a bare operator.
1157        f.write_str(match self {
1158            BinaryOperator::Plus => "+",
1159            BinaryOperator::Minus => "-",
1160            BinaryOperator::Multiply => "*",
1161            BinaryOperator::Divide => "/",
1162            // Each spelling tag renders its own surface form so the exact source
1163            // round-trips (`%` vs the MySQL `MOD` keyword).
1164            BinaryOperator::Modulo(ModuloSpelling::Percent) => "%",
1165            BinaryOperator::Modulo(ModuloSpelling::Mod) => "MOD",
1166            // One integer-division operator; the spelling tag restores the exact source
1167            // form. Load-bearing, not cosmetic: DuckDB has no `DIV` keyword and MySQL no
1168            // `//` operator, so a normalized spelling would not re-parse (like the XOR tags).
1169            BinaryOperator::IntegerDivide(IntegerDivideSpelling::Div) => "DIV",
1170            BinaryOperator::IntegerDivide(IntegerDivideSpelling::SlashSlash) => "//",
1171            // PostgreSQL exponentiation. Distinct from the MySQL `^` bitwise-XOR spelling
1172            // above (`BitwiseXor(Caret)`): same glyph, different operator and precedence.
1173            BinaryOperator::Exponent => "^",
1174            BinaryOperator::StringConcat => "||",
1175            BinaryOperator::Contains => "@>",
1176            BinaryOperator::ContainedBy => "<@",
1177            BinaryOperator::Overlap => "&&",
1178            BinaryOperator::JsonGet => "->",
1179            BinaryOperator::JsonGetText => "->>",
1180            BinaryOperator::JsonExists => "?",
1181            BinaryOperator::JsonExistsAny => "?|",
1182            BinaryOperator::JsonExistsAll => "?&",
1183            BinaryOperator::JsonPathExists => "@?",
1184            BinaryOperator::JsonPathMatch => "@@",
1185            BinaryOperator::JsonExtractPath => "#>",
1186            BinaryOperator::JsonExtractPathText => "#>>",
1187            BinaryOperator::JsonDeletePath => "#-",
1188            BinaryOperator::BitwiseOr => "|",
1189            BinaryOperator::BitwiseAnd => "&",
1190            BinaryOperator::BitwiseShiftLeft => "<<",
1191            BinaryOperator::BitwiseShiftRight => ">>",
1192            // The two XOR spellings restore the exact source form; this is load-bearing,
1193            // not cosmetic (PostgreSQL rejects `^` as XOR, MySQL treats `#` as a comment).
1194            BinaryOperator::BitwiseXor(BitwiseXorSpelling::Hash) => "#",
1195            BinaryOperator::BitwiseXor(BitwiseXorSpelling::Caret) => "^",
1196            // One equality operator; the spelling tag restores the exact source form
1197            // (`=` vs the SQLite `==`), mirroring the modulo/regex tags above.
1198            BinaryOperator::Eq(EqualsSpelling::Single) => "=",
1199            BinaryOperator::Eq(EqualsSpelling::Double) => "==",
1200            // One inequality operator; the spelling tag restores the exact source form
1201            // (the SQL-standard `<>` vs the C-style `!=`). Both spellings parse under
1202            // every dialect, so a target re-spell and the redacted fingerprint normalize
1203            // to the canonical `<>` (unlike the `==`/`DIV`/XOR tags, this is not
1204            // load-bearing for validity), keeping the fingerprint stable.
1205            BinaryOperator::NotEq(NotEqSpelling::Bang) if honours_source_spelling(ctx) => "!=",
1206            BinaryOperator::NotEq(_) => "<>",
1207            BinaryOperator::Lt => "<",
1208            BinaryOperator::LtEq => "<=",
1209            BinaryOperator::Gt => ">",
1210            BinaryOperator::GtEq => ">=",
1211            // One null-safe-inequality operator; the spelling tag restores the exact
1212            // source form (the `IS DISTINCT FROM` keyword vs SQLite's bare `IS NOT`).
1213            BinaryOperator::IsDistinctFrom(IsDistinctFromSpelling::Keyword) => "IS DISTINCT FROM",
1214            BinaryOperator::IsDistinctFrom(IsDistinctFromSpelling::Is) => "IS NOT",
1215            // One null-safe-equality operator; the spelling tag restores the exact source
1216            // form. The keyword/`<=>` split is load-bearing, not cosmetic: MySQL rejects the
1217            // keyword form and the other dialects reject `<=>`, so a normalized render would
1218            // not re-parse. SQLite's bare `IS` folds on here too and renders back as `IS`.
1219            BinaryOperator::IsNotDistinctFrom(IsNotDistinctFromSpelling::Keyword) => {
1220                "IS NOT DISTINCT FROM"
1221            }
1222            BinaryOperator::IsNotDistinctFrom(IsNotDistinctFromSpelling::NullSafeEq) => "<=>",
1223            BinaryOperator::IsNotDistinctFrom(IsNotDistinctFromSpelling::Is) => "IS",
1224            BinaryOperator::Regexp(RegexpSpelling::Rlike) => "RLIKE",
1225            BinaryOperator::Regexp(RegexpSpelling::Regexp) => "REGEXP",
1226            BinaryOperator::Glob => "GLOB",
1227            BinaryOperator::StartsWith => "^@",
1228            BinaryOperator::Match => "MATCH",
1229            BinaryOperator::Overlaps => "OVERLAPS",
1230            BinaryOperator::And => "AND",
1231            BinaryOperator::Xor => "XOR",
1232            BinaryOperator::Or => "OR",
1233        })
1234    }
1235}
1236
1237impl Render for UnaryOperator {
1238    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1239        f.write_str(match self {
1240            UnaryOperator::Not => "NOT",
1241            UnaryOperator::Minus => "-",
1242            UnaryOperator::Plus => "+",
1243            UnaryOperator::BitwiseNot => "~",
1244            UnaryOperator::Prior => "PRIOR",
1245        })
1246    }
1247}
1248
1249impl Render for SetOperator {
1250    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1251        f.write_str(match self {
1252            SetOperator::Union => "UNION",
1253            SetOperator::Intersect => "INTERSECT",
1254            SetOperator::Except => "EXCEPT",
1255        })
1256    }
1257}
1258
1259impl Render for Quantifier {
1260    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1261        f.write_str(match self {
1262            Quantifier::Any => "ANY",
1263            Quantifier::All => "ALL",
1264            Quantifier::Some => "SOME",
1265        })
1266    }
1267}
1268
1269impl Render for SetQuantifier {
1270    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1271        f.write_str(match self {
1272            SetQuantifier::All => "ALL",
1273            SetQuantifier::Distinct => "DISTINCT",
1274        })
1275    }
1276}
1277
1278// ---------------------------------------------------------------------------
1279// Expressions and bp-derived parenthesization
1280// ---------------------------------------------------------------------------
1281
1282impl<X: Extension + Render> Render for Expr<X> {
1283    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1284        match self {
1285            Expr::Column { name, .. } => name.render(ctx, f),
1286            Expr::Literal { literal, .. } => literal.render(ctx, f),
1287            Expr::BinaryOp {
1288                left, op, right, ..
1289            } => {
1290                let full = ctx.mode() == RenderMode::Parenthesized;
1291                open_group(full, f)?;
1292                render_operand(
1293                    left,
1294                    binary_child_needs_parens(&ctx.target().binding_powers, op, left, Side::Left),
1295                    ctx,
1296                    f,
1297                )?;
1298                f.write_str(" ")?;
1299                op.render(ctx, f)?;
1300                f.write_str(" ")?;
1301                render_operand(
1302                    right,
1303                    binary_child_needs_parens(&ctx.target().binding_powers, op, right, Side::Right),
1304                    ctx,
1305                    f,
1306                )?;
1307                close_group(full, f)
1308            }
1309            Expr::UnaryOp { op, expr, .. } => {
1310                let full = ctx.mode() == RenderMode::Parenthesized;
1311                open_group(full, f)?;
1312                op.render(ctx, f)?;
1313                // `NOT` and `PRIOR` are alphabetic and must be separated from their
1314                // operand; the sign operators bind tight against theirs (`-a`, `+a`).
1315                if matches!(op, UnaryOperator::Not | UnaryOperator::Prior) {
1316                    f.write_str(" ")?;
1317                }
1318                render_operand(
1319                    expr,
1320                    prefix_operand_needs_parens(&ctx.target().binding_powers, op, expr),
1321                    ctx,
1322                    f,
1323                )?;
1324                close_group(full, f)
1325            }
1326            Expr::Function { call, .. } => call.render(ctx, f),
1327            Expr::Case { case, .. } => case.render(ctx, f),
1328            Expr::Extract { extract, .. } => extract.render(ctx, f),
1329            Expr::JsonFunc { json_func, .. } => json_func.render(ctx, f),
1330            Expr::JsonObject { json_object, .. } => json_object.render(ctx, f),
1331            Expr::JsonArray { json_array, .. } => json_array.render(ctx, f),
1332            Expr::JsonAggregate { json_aggregate, .. } => json_aggregate.render(ctx, f),
1333            Expr::JsonConstructor {
1334                json_constructor, ..
1335            } => json_constructor.render(ctx, f),
1336            Expr::IsJson { is_json, .. } => is_json.render(ctx, f),
1337            Expr::XmlFunc { xml_func, .. } => xml_func.render(ctx, f),
1338            Expr::StringFunc { string_func, .. } => string_func.render(ctx, f),
1339            Expr::IsDocument { expr, negated, .. } => {
1340                let full = ctx.mode() == RenderMode::Parenthesized;
1341                open_group(full, f)?;
1342                render_predicate_operand(
1343                    expr,
1344                    ctx.target().binding_powers.predicate(),
1345                    Side::Left,
1346                    ctx,
1347                    f,
1348                )?;
1349                f.write_str(if *negated {
1350                    " IS NOT DOCUMENT"
1351                } else {
1352                    " IS DOCUMENT"
1353                })?;
1354                close_group(full, f)
1355            }
1356            Expr::Cast {
1357                expr,
1358                data_type,
1359                syntax,
1360                try_cast,
1361                ..
1362            } => match syntax {
1363                // The `try` flag only reaches the `Call` spelling — DuckDB's `TRY_CAST`
1364                // has no `::`/prefix form — so it selects the `TRY_CAST(` vs `CAST(` lead.
1365                CastSyntax::Call => {
1366                    f.write_str(if *try_cast { "TRY_CAST(" } else { "CAST(" })?;
1367                    expr.render(ctx, f)?;
1368                    f.write_str(" AS ")?;
1369                    data_type.render(ctx, f)?;
1370                    f.write_str(")")
1371                }
1372                CastSyntax::DoubleColon => {
1373                    let full = ctx.mode() == RenderMode::Parenthesized;
1374                    open_group(full, f)?;
1375                    render_pg_operand(
1376                        expr,
1377                        ctx.target().binding_powers.typecast,
1378                        Side::Left,
1379                        ctx,
1380                        f,
1381                    )?;
1382                    f.write_str("::")?;
1383                    data_type.render(ctx, f)?;
1384                    close_group(full, f)
1385                }
1386                // `type 'string'`: the type name ahead of its string constant. It is a
1387                // primary (atom-like, like a `CAST(...)` call), so it never self-wraps
1388                // for the `Parenthesized` oracle mode and its string operand — always a
1389                // literal — renders verbatim without operand parentheses.
1390                CastSyntax::PrefixTyped => {
1391                    data_type.render(ctx, f)?;
1392                    f.write_str(" ")?;
1393                    expr.render(ctx, f)
1394                }
1395                // MySQL comma-form cast — a primary (atom-like) `CONVERT(<expr>, <type>)`
1396                // call, like the `CAST(...)` spelling above.
1397                CastSyntax::Convert => {
1398                    f.write_str("CONVERT(")?;
1399                    expr.render(ctx, f)?;
1400                    f.write_str(", ")?;
1401                    data_type.render(ctx, f)?;
1402                    f.write_str(")")
1403                }
1404            },
1405            Expr::IsNull {
1406                expr,
1407                negated,
1408                spelling,
1409                ..
1410            } => {
1411                let full = ctx.mode() == RenderMode::Parenthesized;
1412                open_group(full, f)?;
1413                render_predicate_operand(
1414                    expr,
1415                    ctx.target().binding_powers.predicate(),
1416                    Side::Left,
1417                    ctx,
1418                    f,
1419                )?;
1420                f.write_str(match (spelling, negated) {
1421                    (NullTestSpelling::Is, false) => " IS NULL",
1422                    (NullTestSpelling::Is, true) => " IS NOT NULL",
1423                    (NullTestSpelling::Postfix, false) => " ISNULL",
1424                    (NullTestSpelling::Postfix, true) => " NOTNULL",
1425                    // The two-word postfix is only ever produced with `negated: true`.
1426                    (NullTestSpelling::PostfixNotNull, _) => " NOT NULL",
1427                })?;
1428                close_group(full, f)
1429            }
1430            Expr::IsTruth {
1431                expr,
1432                value,
1433                negated,
1434                ..
1435            } => {
1436                let full = ctx.mode() == RenderMode::Parenthesized;
1437                open_group(full, f)?;
1438                render_predicate_operand(
1439                    expr,
1440                    ctx.target().binding_powers.predicate(),
1441                    Side::Left,
1442                    ctx,
1443                    f,
1444                )?;
1445                f.write_str(match (negated, value) {
1446                    (false, TruthValue::True) => " IS TRUE",
1447                    (true, TruthValue::True) => " IS NOT TRUE",
1448                    (false, TruthValue::False) => " IS FALSE",
1449                    (true, TruthValue::False) => " IS NOT FALSE",
1450                    (false, TruthValue::Unknown) => " IS UNKNOWN",
1451                    (true, TruthValue::Unknown) => " IS NOT UNKNOWN",
1452                })?;
1453                close_group(full, f)
1454            }
1455            Expr::IsNormalized {
1456                expr,
1457                form,
1458                negated,
1459                ..
1460            } => {
1461                let full = ctx.mode() == RenderMode::Parenthesized;
1462                open_group(full, f)?;
1463                render_predicate_operand(
1464                    expr,
1465                    ctx.target().binding_powers.predicate(),
1466                    Side::Left,
1467                    ctx,
1468                    f,
1469                )?;
1470                f.write_str(if *negated { " IS NOT " } else { " IS " })?;
1471                if let Some(form) = form {
1472                    f.write_str(match form {
1473                        NormalizationForm::Nfc => "NFC ",
1474                        NormalizationForm::Nfd => "NFD ",
1475                        NormalizationForm::Nfkc => "NFKC ",
1476                        NormalizationForm::Nfkd => "NFKD ",
1477                    })?;
1478                }
1479                f.write_str("NORMALIZED")?;
1480                close_group(full, f)
1481            }
1482            Expr::Between {
1483                expr,
1484                low,
1485                high,
1486                negated,
1487                symmetric,
1488                ..
1489            } => {
1490                let full = ctx.mode() == RenderMode::Parenthesized;
1491                open_group(full, f)?;
1492                let range = ctx.target().binding_powers.range_predicate();
1493                render_predicate_operand(expr, range, Side::Left, ctx, f)?;
1494                f.write_str(if *negated {
1495                    " NOT BETWEEN "
1496                } else {
1497                    " BETWEEN "
1498                })?;
1499                if *symmetric {
1500                    f.write_str("SYMMETRIC ")?;
1501                }
1502                render_predicate_operand(low, range, Side::Right, ctx, f)?;
1503                f.write_str(" AND ")?;
1504                render_predicate_operand(high, range, Side::Right, ctx, f)?;
1505                close_group(full, f)
1506            }
1507            Expr::Like {
1508                expr,
1509                pattern,
1510                escape,
1511                negated,
1512                spelling,
1513                ..
1514            } => {
1515                let full = ctx.mode() == RenderMode::Parenthesized;
1516                open_group(full, f)?;
1517                let range = ctx.target().binding_powers.range_predicate();
1518                render_predicate_operand(expr, range, Side::Left, ctx, f)?;
1519                f.write_str(match (negated, spelling) {
1520                    (false, LikeSpelling::Like) => " LIKE ",
1521                    (true, LikeSpelling::Like) => " NOT LIKE ",
1522                    (false, LikeSpelling::ILike) => " ILIKE ",
1523                    (true, LikeSpelling::ILike) => " NOT ILIKE ",
1524                    (false, LikeSpelling::SimilarTo) => " SIMILAR TO ",
1525                    (true, LikeSpelling::SimilarTo) => " NOT SIMILAR TO ",
1526                })?;
1527                render_predicate_operand(pattern, range, Side::Right, ctx, f)?;
1528                if let Some(escape) = escape {
1529                    f.write_str(" ESCAPE ")?;
1530                    render_predicate_operand(escape, range, Side::Right, ctx, f)?;
1531                }
1532                close_group(full, f)
1533            }
1534            Expr::InList {
1535                expr,
1536                list,
1537                negated,
1538                ..
1539            } => {
1540                let full = ctx.mode() == RenderMode::Parenthesized;
1541                open_group(full, f)?;
1542                render_predicate_operand(
1543                    expr,
1544                    ctx.target().binding_powers.range_predicate(),
1545                    Side::Left,
1546                    ctx,
1547                    f,
1548                )?;
1549                f.write_str(if *negated { " NOT IN (" } else { " IN (" })?;
1550                render_comma_separated(list, ctx, f)?;
1551                f.write_str(")")?;
1552                close_group(full, f)
1553            }
1554            Expr::InSubquery {
1555                expr,
1556                subquery,
1557                negated,
1558                ..
1559            } => {
1560                let full = ctx.mode() == RenderMode::Parenthesized;
1561                open_group(full, f)?;
1562                render_predicate_operand(
1563                    expr,
1564                    ctx.target().binding_powers.range_predicate(),
1565                    Side::Left,
1566                    ctx,
1567                    f,
1568                )?;
1569                f.write_str(if *negated { " NOT IN " } else { " IN " })?;
1570                render_query_in_parens(subquery, ctx, f)?;
1571                close_group(full, f)
1572            }
1573            Expr::InExpr {
1574                expr, rhs, negated, ..
1575            } => {
1576                // DuckDB's unparenthesized `IN <value>` binds at its own rank
1577                // (`UNPARENTHESIZED_IN_LIST`), tighter than the comparison predicates, so
1578                // both operands parenthesize by the binding-power oracle against that rank
1579                // rather than the predicate level.
1580                let table = &ctx.target().binding_powers;
1581                let full = ctx.mode() == RenderMode::Parenthesized;
1582                open_group(full, f)?;
1583                render_operand(
1584                    expr,
1585                    operand_needs_parens(table, UNPARENTHESIZED_IN_LIST, expr, Side::Left),
1586                    ctx,
1587                    f,
1588                )?;
1589                f.write_str(if *negated { " NOT IN " } else { " IN " })?;
1590                render_operand(
1591                    rhs,
1592                    operand_needs_parens(table, UNPARENTHESIZED_IN_LIST, rhs, Side::Right),
1593                    ctx,
1594                    f,
1595                )?;
1596                close_group(full, f)
1597            }
1598            Expr::Exists { query, .. } => {
1599                let full = ctx.mode() == RenderMode::Parenthesized;
1600                open_group(full, f)?;
1601                f.write_str("EXISTS ")?;
1602                render_query_in_parens(query, ctx, f)?;
1603                close_group(full, f)
1604            }
1605            Expr::QuantifiedComparison {
1606                left,
1607                op,
1608                quantifier,
1609                subquery,
1610                ..
1611            } => {
1612                let full = ctx.mode() == RenderMode::Parenthesized;
1613                open_group(full, f)?;
1614                render_predicate_operand(
1615                    left,
1616                    ctx.target().binding_powers.comparison,
1617                    Side::Left,
1618                    ctx,
1619                    f,
1620                )?;
1621                f.write_str(" ")?;
1622                op.render(ctx, f)?;
1623                f.write_str(" ")?;
1624                quantifier.render(ctx, f)?;
1625                f.write_str(" ")?;
1626                render_query_in_parens(subquery, ctx, f)?;
1627                close_group(full, f)
1628            }
1629            Expr::QuantifiedList {
1630                left,
1631                op,
1632                quantifier,
1633                array,
1634                ..
1635            } => {
1636                let full = ctx.mode() == RenderMode::Parenthesized;
1637                open_group(full, f)?;
1638                render_predicate_operand(
1639                    left,
1640                    ctx.target().binding_powers.comparison,
1641                    Side::Left,
1642                    ctx,
1643                    f,
1644                )?;
1645                f.write_str(" ")?;
1646                op.render(ctx, f)?;
1647                f.write_str(" ")?;
1648                quantifier.render(ctx, f)?;
1649                f.write_str(" (")?;
1650                array.render(ctx, f)?;
1651                f.write_str(")")?;
1652                close_group(full, f)
1653            }
1654            Expr::QuantifiedLike {
1655                left,
1656                pattern,
1657                quantifier,
1658                negated,
1659                spelling,
1660                ..
1661            } => {
1662                let full = ctx.mode() == RenderMode::Parenthesized;
1663                open_group(full, f)?;
1664                render_predicate_operand(
1665                    left,
1666                    ctx.target().binding_powers.range_predicate(),
1667                    Side::Left,
1668                    ctx,
1669                    f,
1670                )?;
1671                f.write_str(match (negated, spelling) {
1672                    (false, LikeSpelling::Like) => " LIKE ",
1673                    (true, LikeSpelling::Like) => " NOT LIKE ",
1674                    (false, LikeSpelling::ILike) => " ILIKE ",
1675                    (true, LikeSpelling::ILike) => " NOT ILIKE ",
1676                    // `SIMILAR TO` has no quantified form; the parser never builds it, so
1677                    // these arms are unreachable but kept total for the match.
1678                    (false, LikeSpelling::SimilarTo) => " SIMILAR TO ",
1679                    (true, LikeSpelling::SimilarTo) => " NOT SIMILAR TO ",
1680                })?;
1681                quantifier.render(ctx, f)?;
1682                f.write_str(" (")?;
1683                pattern.render(ctx, f)?;
1684                f.write_str(")")?;
1685                close_group(full, f)
1686            }
1687            Expr::Subquery { query, .. } => render_query_in_parens(query, ctx, f),
1688            Expr::Parameter { kind, .. } => match kind {
1689                ParameterKind::Positional(index) => write!(f, "${index}"),
1690                ParameterKind::Numbered(index) => write!(f, "?{index}"),
1691                ParameterKind::Anonymous => f.write_str("?"),
1692                // A placeholder's identity — the positional index above, or the sigil
1693                // and name here — is query structure, not a value, so it renders
1694                // verbatim in every mode (a name is never masked the way a `Literal`
1695                // value is). The sigil tag restores the original spelling.
1696                ParameterKind::Named { name, sigil } => {
1697                    let sigil = match sigil {
1698                        ParameterSigil::Colon => ':',
1699                        ParameterSigil::At => '@',
1700                        ParameterSigil::Dollar => '$',
1701                    };
1702                    write!(f, "{sigil}{}", ctx.resolve(*name))
1703                }
1704            },
1705            // A positional column reference's identity — its 1-based index — is query
1706            // structure, not a value, so it renders verbatim in every mode (never masked
1707            // the way a `Literal` value is), like the positional parameter above.
1708            Expr::PositionalColumn { index, .. } => write!(f, "#{index}"),
1709            // A session variable's identity — its sigil, optional scope, and name — is
1710            // query structure, not a value, so it renders verbatim in every mode (the
1711            // name is never masked the way a `Literal` value is), like the named
1712            // placeholder above. The `kind` tag restores the sigil and the canonical
1713            // lowercase scope keyword so all four forms round-trip.
1714            Expr::SessionVariable { kind, name, .. } => {
1715                let prefix = match kind {
1716                    SessionVariableKind::User => "@",
1717                    SessionVariableKind::System => "@@",
1718                    SessionVariableKind::SystemGlobal => "@@global.",
1719                    SessionVariableKind::SystemSession => "@@session.",
1720                };
1721                write!(f, "{prefix}{}", ctx.resolve(*name))
1722            }
1723            Expr::Subscript { subscript, .. } => {
1724                let full = ctx.mode() == RenderMode::Parenthesized;
1725                open_group(full, f)?;
1726                // A `::`-cast base must be parenthesized even though the typecast binds
1727                // tighter than the subscript: the cast's rendered type-tail would
1728                // otherwise re-absorb the following `[` as an array-type suffix
1729                // (`a::INT[1]` is a cast to `INT[1]`, not a subscript of `a::INT`), so
1730                // binding power alone would wrongly drop the parens. The `CAST(...)` call
1731                // and prefixed `TYPE 'string'` spellings self-delimit and are unaffected.
1732                let base_needs_parens = matches!(
1733                    subscript.base,
1734                    Expr::Cast {
1735                        syntax: CastSyntax::DoubleColon,
1736                        ..
1737                    }
1738                ) || operand_needs_parens(
1739                    &ctx.target().binding_powers,
1740                    ctx.target().binding_powers.subscript,
1741                    &subscript.base,
1742                    Side::Left,
1743                );
1744                render_operand(&subscript.base, base_needs_parens, ctx, f)?;
1745                f.write_str("[")?;
1746                match subscript.kind {
1747                    SubscriptKind::Index => {
1748                        // A bare index carries its single value in `lower`.
1749                        if let Some(index) = &subscript.lower {
1750                            index.render(ctx, f)?;
1751                        }
1752                    }
1753                    SubscriptKind::Slice => {
1754                        if let Some(lower) = &subscript.lower {
1755                            lower.render(ctx, f)?;
1756                        }
1757                        f.write_str(":")?;
1758                        if let Some(upper) = &subscript.upper {
1759                            upper.render(ctx, f)?;
1760                        }
1761                    }
1762                    SubscriptKind::SliceWithStep => {
1763                        if let Some(lower) = &subscript.lower {
1764                            lower.render(ctx, f)?;
1765                        }
1766                        f.write_str(":")?;
1767                        // The middle bound is mandatory; an omitted upper is DuckDB's `-`
1768                        // open-upper placeholder, so a `None` renders as `-`, not empty.
1769                        match &subscript.upper {
1770                            Some(upper) => upper.render(ctx, f)?,
1771                            None => f.write_str("-")?,
1772                        }
1773                        f.write_str(":")?;
1774                        if let Some(step) = &subscript.step {
1775                            step.render(ctx, f)?;
1776                        }
1777                    }
1778                }
1779                f.write_str("]")?;
1780                close_group(full, f)
1781            }
1782            Expr::SemiStructuredAccess {
1783                semi_structured_access,
1784                ..
1785            } => semi_structured_access.render(ctx, f),
1786            Expr::Collate { collate, .. } => {
1787                let full = ctx.mode() == RenderMode::Parenthesized;
1788                open_group(full, f)?;
1789                render_pg_operand(
1790                    &collate.expr,
1791                    ctx.target().binding_powers.collate,
1792                    Side::Left,
1793                    ctx,
1794                    f,
1795                )?;
1796                f.write_str(" COLLATE ")?;
1797                collate.collation.render(ctx, f)?;
1798                close_group(full, f)
1799            }
1800            Expr::AtTimeZone { at_time_zone, .. } => {
1801                let full = ctx.mode() == RenderMode::Parenthesized;
1802                open_group(full, f)?;
1803                let bp = ctx.target().binding_powers.at_time_zone;
1804                render_pg_operand(&at_time_zone.expr, bp, Side::Left, ctx, f)?;
1805                f.write_str(" AT TIME ZONE ")?;
1806                // The zone is the right operand, parsed at the operator's right
1807                // binding power, so it parenthesizes by the same rule.
1808                render_pg_operand(&at_time_zone.zone, bp, Side::Right, ctx, f)?;
1809                close_group(full, f)
1810            }
1811            Expr::Interval { value, unit, .. } => {
1812                // The MySQL operator-position interval quantity, a primary (highest binding
1813                // power): the amount is terminated by the unit keyword, so it renders as a bare
1814                // sub-expression with no operator-precedence parens. The unit reuses the shared
1815                // IntervalFields vocabulary in MySQL underscore spelling (never the ANSI `TO`
1816                // form); its suffix carries its own leading space.
1817                let full = ctx.mode() == RenderMode::Parenthesized;
1818                open_group(full, f)?;
1819                f.write_str("INTERVAL ")?;
1820                value.render(ctx, f)?;
1821                f.write_str(mysql_interval_unit_suffix(*unit))?;
1822                close_group(full, f)
1823            }
1824            Expr::Array { array, .. } => match &**array {
1825                ArrayExpr::Elements {
1826                    elements, spelling, ..
1827                } => {
1828                    f.write_str(match spelling {
1829                        ArraySpelling::Keyword => "ARRAY[",
1830                        ArraySpelling::Bracket => "[",
1831                    })?;
1832                    render_comma_separated(elements, ctx, f)?;
1833                    f.write_str("]")
1834                }
1835                ArrayExpr::Subquery { query, .. } => {
1836                    f.write_str("ARRAY")?;
1837                    render_query_in_parens(query, ctx, f)
1838                }
1839                ArrayExpr::Comprehension { comprehension, .. } => {
1840                    f.write_str("[")?;
1841                    comprehension.element.render(ctx, f)?;
1842                    f.write_str(" for ")?;
1843                    render_ident_list(&comprehension.vars, ctx, f)?;
1844                    f.write_str(" in ")?;
1845                    render_comprehension_source(&comprehension.source, ctx, f)?;
1846                    if let Some(filter) = &comprehension.filter {
1847                        f.write_str(" if ")?;
1848                        filter.render(ctx, f)?;
1849                    }
1850                    f.write_str("]")
1851                }
1852            },
1853            Expr::Struct { r#struct, .. } => {
1854                f.write_str("{")?;
1855                for (i, field) in r#struct.fields.iter().enumerate() {
1856                    if i > 0 {
1857                        f.write_str(", ")?;
1858                    }
1859                    field.render(ctx, f)?;
1860                }
1861                f.write_str("}")
1862            }
1863            Expr::StructConstructor { constructor, .. } => {
1864                f.write_str("STRUCT")?;
1865                if !constructor.fields.is_empty() {
1866                    f.write_str("<")?;
1867                    render_comma_separated(&constructor.fields, ctx, f)?;
1868                    f.write_str(">")?;
1869                }
1870                f.write_str("(")?;
1871                render_comma_separated(&constructor.args, ctx, f)?;
1872                f.write_str(")")
1873            }
1874            Expr::Map { map, .. } => {
1875                f.write_str("MAP {")?;
1876                for (i, entry) in map.entries.iter().enumerate() {
1877                    if i > 0 {
1878                        f.write_str(", ")?;
1879                    }
1880                    entry.key.render(ctx, f)?;
1881                    f.write_str(": ")?;
1882                    entry.value.render(ctx, f)?;
1883                }
1884                f.write_str("}")
1885            }
1886            Expr::Row { row, .. } => {
1887                f.write_str(if row.explicit { "ROW(" } else { "(" })?;
1888                render_comma_separated(&row.fields, ctx, f)?;
1889                f.write_str(")")
1890            }
1891            Expr::FieldSelection {
1892                field_selection, ..
1893            } => {
1894                // The base is always parenthesized so `(c).f` cannot re-parse as the
1895                // qualified column `c.f`; the `.*` star form keeps the same parens so a
1896                // whole-row `tbl.*` value renders `(tbl).*` (re-parses to the same node).
1897                f.write_str("(")?;
1898                field_selection.base.render(ctx, f)?;
1899                f.write_str(").")?;
1900                match &field_selection.selector {
1901                    FieldSelector::Field { field, .. } => field.render(ctx, f),
1902                    FieldSelector::Star { .. } => f.write_str("*"),
1903                }
1904            }
1905            Expr::SpecialFunction {
1906                keyword, precision, ..
1907            } => {
1908                f.write_str(special_function_keyword(*keyword))?;
1909                if let Some(precision) = precision {
1910                    write!(f, "({precision})")?;
1911                }
1912                Ok(())
1913            }
1914            // A PostgreSQL infix operator at the "any other operator" rank (the
1915            // `any_operator` level, ADR-0008), so it routes through the same
1916            // binding-power-driven operand parenthesization as the extension infix
1917            // operators. The bare spelling renders `a op b` (`a ~ b`, `a <-> b`); the
1918            // explicit form renders `a OPERATOR(schema.op) b`, carrying its optional schema
1919            // qualification. PostgreSQL's own deparse keeps the two apart (a bare `~` stays
1920            // bare, `OPERATOR(pg_catalog.+)` stays wrapped), so the spelling tag is
1921            // load-bearing for round-trip fidelity.
1922            Expr::NamedOperator { named_operator, .. } => render_extension_infix(
1923                ctx,
1924                f,
1925                ctx.target().binding_powers.any_operator,
1926                (&named_operator.left, &named_operator.right),
1927                |f| match named_operator.spelling {
1928                    NamedOperatorSpelling::Bare => {
1929                        f.write_str(" ")?;
1930                        f.write_str(ctx.resolve(named_operator.op))?;
1931                        f.write_str(" ")
1932                    }
1933                    NamedOperatorSpelling::OperatorKeyword => {
1934                        f.write_str(" OPERATOR(")?;
1935                        for part in &named_operator.schema.0 {
1936                            part.render(ctx, f)?;
1937                            f.write_str(".")?;
1938                        }
1939                        f.write_str(ctx.resolve(named_operator.op))?;
1940                        f.write_str(") ")
1941                    }
1942                },
1943            ),
1944            // A PostgreSQL prefix operator (`@ x`, `|/ x`, `@@ box`, `@#@ x`) at the "any
1945            // other operator" rank. The trailing space keeps the operator a bare token and
1946            // stops the operand's own lead byte (`-5`, another operator) from re-lexing into
1947            // a longer operator.
1948            Expr::PrefixOperator {
1949                prefix_operator, ..
1950            } => render_extension_prefix(
1951                ctx,
1952                f,
1953                ctx.target().binding_powers.any_operator.left,
1954                |f| {
1955                    f.write_str(ctx.resolve(prefix_operator.op))?;
1956                    f.write_str(" ")
1957                },
1958                &prefix_operator.operand,
1959            ),
1960            // A DuckDB postfix operator (`10 !`, `1 ~`, `1 <->`) at the "any other operator"
1961            // rank. The leading space keeps the operator a bare token so the operand's own
1962            // trailing byte does not re-lex into a longer operator.
1963            Expr::PostfixOperator {
1964                postfix_operator, ..
1965            } => render_extension_postfix(
1966                ctx,
1967                f,
1968                ctx.target().binding_powers.any_operator,
1969                &postfix_operator.operand,
1970                |f| {
1971                    f.write_str(" ")?;
1972                    f.write_str(ctx.resolve(postfix_operator.op))
1973                },
1974            ),
1975            // The DuckDB lambda `->` is the JSON-arrow token at the JSON-arrow rank,
1976            // so its body parenthesizes as that operator's right operand. The
1977            // parameter side is closed (bare idents), never parenthesized beyond its
1978            // recorded spelling.
1979            Expr::Lambda { lambda, .. } => {
1980                let full = ctx.mode() == RenderMode::Parenthesized;
1981                open_group(full, f)?;
1982                match lambda.spelling {
1983                    // The python-style spelling `lambda <params>: <body>`: the `:` and the
1984                    // enclosing delimiter bound the body, so it renders like a function
1985                    // argument — no operator-precedence parens, unlike the arrow forms.
1986                    LambdaParamSpelling::Keyword => {
1987                        f.write_str("lambda ")?;
1988                        render_comma_separated(&lambda.params, ctx, f)?;
1989                        f.write_str(": ")?;
1990                        lambda.body.render(ctx, f)?;
1991                    }
1992                    arrow => {
1993                        match arrow {
1994                            // `Bare` implies one parameter (a parser invariant); rendering
1995                            // falls back to the parenthesized list on a synthesized
1996                            // multi-parameter value so output stays re-parseable.
1997                            LambdaParamSpelling::Bare if lambda.params.len() == 1 => {
1998                                lambda.params[0].render(ctx, f)?;
1999                            }
2000                            LambdaParamSpelling::RowKeyword => {
2001                                f.write_str("ROW(")?;
2002                                render_comma_separated(&lambda.params, ctx, f)?;
2003                                f.write_str(")")?;
2004                            }
2005                            // `Parenthesized`, or a synthesized multi-parameter `Bare`.
2006                            _ => {
2007                                f.write_str("(")?;
2008                                render_comma_separated(&lambda.params, ctx, f)?;
2009                                f.write_str(")")?;
2010                            }
2011                        }
2012                        f.write_str(" -> ")?;
2013                        render_pg_operand(
2014                            &lambda.body,
2015                            ctx.target().binding_powers.binary(&BinaryOperator::JsonGet),
2016                            Side::Right,
2017                            ctx,
2018                            f,
2019                        )?;
2020                    }
2021                }
2022                close_group(full, f)
2023            }
2024            // The DuckDB star node in its three spellings (`spelling`): the wrapped
2025            // `COLUMNS(<pattern>)` / star `COLUMNS(*)` / `COLUMNS(t.*)`, the `*COLUMNS(…)`
2026            // unpack prefix, and the bare `*` / `t.*` written without the wrapper — each
2027            // carrying the wildcard modifiers on its star form (`COLUMNS(* EXCLUDE (i))`,
2028            // `* EXCLUDE (id)`). An atom like a call: no self-parenthesization, the parent
2029            // decides grouping.
2030            Expr::Columns {
2031                qualifier,
2032                pattern,
2033                options,
2034                spelling,
2035                ..
2036            } => {
2037                if matches!(spelling, ColumnsSpelling::Star) {
2038                    // The bare star has no `COLUMNS(...)` wrapper and never a pattern.
2039                    if let Some(qualifier) = qualifier {
2040                        qualifier.render(ctx, f)?;
2041                        f.write_str(".")?;
2042                    }
2043                    f.write_str("*")?;
2044                    if let Some(options) = options {
2045                        render_wildcard_options(options, ctx, f)?;
2046                    }
2047                    return Ok(());
2048                }
2049                if matches!(spelling, ColumnsSpelling::Unpack) {
2050                    f.write_str("*")?;
2051                }
2052                f.write_str("COLUMNS(")?;
2053                match pattern {
2054                    Some(pattern) => pattern.render(ctx, f)?,
2055                    None => {
2056                        if let Some(qualifier) = qualifier {
2057                            qualifier.render(ctx, f)?;
2058                            f.write_str(".")?;
2059                        }
2060                        f.write_str("*")?;
2061                        if let Some(options) = options {
2062                            render_wildcard_options(options, ctx, f)?;
2063                        }
2064                    }
2065                }
2066                f.write_str(")")
2067            }
2068            Expr::Other { ext, .. } => ext.render(ctx, f),
2069        }
2070    }
2071}
2072
2073/// The canonical uppercase spelling of a SQL special value function keyword.
2074fn special_function_keyword(keyword: SpecialFunctionKeyword) -> &'static str {
2075    match keyword {
2076        SpecialFunctionKeyword::CurrentCatalog => "CURRENT_CATALOG",
2077        SpecialFunctionKeyword::CurrentDate => "CURRENT_DATE",
2078        SpecialFunctionKeyword::CurrentRole => "CURRENT_ROLE",
2079        SpecialFunctionKeyword::CurrentSchema => "CURRENT_SCHEMA",
2080        SpecialFunctionKeyword::CurrentTime => "CURRENT_TIME",
2081        SpecialFunctionKeyword::CurrentTimestamp => "CURRENT_TIMESTAMP",
2082        SpecialFunctionKeyword::CurrentUser => "CURRENT_USER",
2083        SpecialFunctionKeyword::LocalTime => "LOCALTIME",
2084        SpecialFunctionKeyword::LocalTimestamp => "LOCALTIMESTAMP",
2085        SpecialFunctionKeyword::SessionUser => "SESSION_USER",
2086        SpecialFunctionKeyword::SystemUser => "SYSTEM_USER",
2087        SpecialFunctionKeyword::User => "USER",
2088        SpecialFunctionKeyword::UtcDate => "UTC_DATE",
2089        SpecialFunctionKeyword::UtcTime => "UTC_TIME",
2090        SpecialFunctionKeyword::UtcTimestamp => "UTC_TIMESTAMP",
2091    }
2092}
2093
2094fn open_group(group: bool, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2095    if group {
2096        f.write_str("(")?;
2097    }
2098    Ok(())
2099}
2100
2101fn close_group(group: bool, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2102    if group {
2103        f.write_str(")")?;
2104    }
2105    Ok(())
2106}
2107
2108/// Render a child expression, adding parentheses when required.
2109///
2110/// In `Parenthesized` mode every binary/unary node already wraps itself, so the
2111/// parent contributes nothing; in the other modes it adds exactly the parens the
2112/// binding-power table demands (`canonical_parens`).
2113fn render_operand<X: Extension + Render>(
2114    child: &Expr<X>,
2115    canonical_parens: bool,
2116    ctx: &RenderCtx<'_>,
2117    f: &mut fmt::Formatter<'_>,
2118) -> fmt::Result {
2119    let wrap = match ctx.mode() {
2120        RenderMode::Parenthesized => false,
2121        RenderMode::Canonical | RenderMode::Redacted => canonical_parens,
2122    };
2123    open_group(wrap, f)?;
2124    child.render(ctx, f)?;
2125    close_group(wrap, f)
2126}
2127
2128/// Render an operand of an `IS NULL` / `BETWEEN` / `IN` predicate.
2129///
2130/// The operand is parenthesized by the same binding-power rule as any other child rather than
2131/// conservatively whenever it is compound: only when it binds looser than the predicate's own
2132/// level, or is an equal-precedence non-associative sibling. `side` is `Left` for the principal
2133/// operand (left of the keyword) and `Right` for a `BETWEEN` bound / `LIKE` pattern, which the
2134/// parser parses at the predicate's right binding power.
2135///
2136/// `parent` is the binding power the predicate node itself binds at — `bp.predicate()` for the
2137/// `IS`-family (`IS NULL`/`IS TRUE`/`IS DISTINCT FROM`…), `bp.range_predicate()` for the
2138/// range/pattern/membership family (`BETWEEN`/`LIKE`/`IN`…), `bp.comparison` for a quantified
2139/// comparison — so the operand is grouped against the SAME rank the parser climbed the
2140/// predicate at (ADR-0008). Child-shape dispatch is the shared [`operand_needs_parens`] oracle,
2141/// so a predicate operand parenthesizes by exactly the same rule as any other operand position.
2142fn render_predicate_operand<X: Extension + Render>(
2143    child: &Expr<X>,
2144    parent: BindingPower,
2145    side: Side,
2146    ctx: &RenderCtx<'_>,
2147    f: &mut fmt::Formatter<'_>,
2148) -> fmt::Result {
2149    render_operand(
2150        child,
2151        operand_needs_parens(&ctx.target().binding_powers, parent, child, side),
2152        ctx,
2153        f,
2154    )
2155}
2156
2157fn render_query_in_parens<X: Extension + Render>(
2158    query: &Query<X>,
2159    ctx: &RenderCtx<'_>,
2160    f: &mut fmt::Formatter<'_>,
2161) -> fmt::Result {
2162    f.write_str("(")?;
2163    query.render(ctx, f)?;
2164    f.write_str(")")
2165}
2166
2167/// Whether a binary parent's child needs parens on the given side.
2168///
2169/// Binary children defer to the authoritative [`BindingPowerTable::needs_parens`]
2170/// oracle (which also encodes equal-precedence associativity), read from the
2171/// target dialect's table so render-time grouping honours the same per-dialect
2172/// binding powers the parser used. A prefix-operator child closes its
2173/// left edge, so it only needs parens as a *left* operand whose binding power the
2174/// parent's left side outbinds (e.g. `(NOT a) * b`). A comparison-level predicate
2175/// (`IS NULL` / `BETWEEN` / `IN`) binds at the predicate level, so it parenthesizes
2176/// by the same binding-power rule against the parent: `(a IS NULL) = b` wraps
2177/// (equal-precedence, non-associative), while `a IS NULL AND b` stays bare.
2178fn binary_child_needs_parens<X: Extension + Render>(
2179    bp: &BindingPowerTable,
2180    parent: &BinaryOperator,
2181    child: &Expr<X>,
2182    side: Side,
2183) -> bool {
2184    match child {
2185        Expr::BinaryOp { op, .. } => bp.needs_parens(parent, op, side),
2186        Expr::UnaryOp { op, .. } => match side {
2187            Side::Right => false,
2188            Side::Left => bp.prefix(op) < bp.binary(parent).left,
2189        },
2190        Expr::Between { .. }
2191        | Expr::Like { .. }
2192        | Expr::QuantifiedLike { .. }
2193        | Expr::InList { .. }
2194        | Expr::InSubquery { .. } => {
2195            needs_parens_between(bp.binary(parent), bp.range_predicate(), side)
2196        }
2197        Expr::IsNull { .. } | Expr::IsTruth { .. } | Expr::IsNormalized { .. } => {
2198            needs_parens_between(bp.binary(parent), bp.predicate(), side)
2199        }
2200        // A quantified comparison (`x = ANY (…)`) binds at its comparison operator's rank, not
2201        // the `IS`-family predicate tier — the parser climbs it at `binding_power(op)`.
2202        Expr::QuantifiedComparison { .. } => {
2203            needs_parens_between(bp.binary(parent), bp.comparison, side)
2204        }
2205        Expr::InExpr { .. } => {
2206            needs_parens_between(bp.binary(parent), UNPARENTHESIZED_IN_LIST, side)
2207        }
2208        Expr::NamedOperator { .. } => {
2209            needs_parens_between(bp.binary(parent), bp.any_operator, side)
2210        }
2211        Expr::PrefixOperator { .. } => match side {
2212            Side::Right => false,
2213            Side::Left => bp.any_operator.left < bp.binary(parent).left,
2214        },
2215        // A postfix operator is the mirror of a prefix operator: it closes its outer-left
2216        // edge with its operand, so it needs parens only as a looser-binding *right* operand.
2217        Expr::PostfixOperator { .. } => match side {
2218            Side::Left => false,
2219            Side::Right => bp.any_operator.left < bp.binary(parent).right,
2220        },
2221        // A lambda binds at the JSON-arrow rank (it is that token), so it groups as
2222        // a `JsonGet` child of the parent operator.
2223        Expr::Lambda { .. } => {
2224            needs_parens_between(bp.binary(parent), bp.binary(&BinaryOperator::JsonGet), side)
2225        }
2226        Expr::Other { ext, .. } => match ext.operand_binding_power() {
2227            Some(child) => needs_parens_between(bp.binary(parent), child, side),
2228            None => false,
2229        },
2230        _ => false,
2231    }
2232}
2233
2234/// Whether a prefix operator's operand needs parens.
2235///
2236/// A binary operand needs parens when it binds looser than the prefix on the side
2237/// the prefix reaches across (`NOT (a AND b)`, `-(a + b)`), per the target
2238/// dialect's binding-power table. A nested sign operand is parenthesized so the
2239/// spelling does not collide with `--` / `++` tokens. A comparison-level predicate
2240/// operand binds at the predicate level, so it mirrors the binary arm against that
2241/// level: tighter-binding `-` wraps it (`-(a IS NULL)`, else `-a IS NULL` re-parses
2242/// as `(-a) IS NULL`), while looser `NOT` leaves `NOT a IS NULL` bare.
2243fn prefix_operand_needs_parens<X: Extension + Render>(
2244    bp: &BindingPowerTable,
2245    op: &UnaryOperator,
2246    operand: &Expr<X>,
2247) -> bool {
2248    match operand {
2249        Expr::BinaryOp { op: child, .. } => bp.binary(child).left < bp.prefix(op),
2250        Expr::UnaryOp { op: inner, .. } => is_sign(op) && is_sign(inner),
2251        Expr::Between { .. }
2252        | Expr::Like { .. }
2253        | Expr::QuantifiedLike { .. }
2254        | Expr::InList { .. }
2255        | Expr::InSubquery { .. } => bp.range_predicate().left < bp.prefix(op),
2256        Expr::IsNull { .. } | Expr::IsTruth { .. } | Expr::IsNormalized { .. } => {
2257            bp.predicate().left < bp.prefix(op)
2258        }
2259        // A quantified comparison binds at its comparison operator's rank (see
2260        // `binary_child_needs_parens`).
2261        Expr::QuantifiedComparison { .. } => bp.comparison.left < bp.prefix(op),
2262        Expr::InExpr { .. } => UNPARENTHESIZED_IN_LIST.left < bp.prefix(op),
2263        Expr::NamedOperator { .. } => bp.any_operator.left < bp.prefix(op),
2264        // A prefix symbolic operator binds at the "any other operator" rank; a tighter
2265        // sign prefix wraps it (so `-@a` renders `-(@a)`, never the `-@` operator).
2266        Expr::PrefixOperator { .. } => bp.any_operator.left < bp.prefix(op),
2267        // A postfix symbolic operator binds at the "any other operator" rank; a tighter sign
2268        // prefix wraps it (so `-(1!)` never renders `-1!`, which re-parses `(-1)!`).
2269        Expr::PostfixOperator { .. } => bp.any_operator.left < bp.prefix(op),
2270        // A lambda binds at the JSON-arrow rank, so a tighter prefix wraps it.
2271        Expr::Lambda { .. } => bp.binary(&BinaryOperator::JsonGet).left < bp.prefix(op),
2272        Expr::Other { ext, .. } => ext
2273            .operand_binding_power()
2274            .is_some_and(|child| child.left < bp.prefix(op)),
2275        _ => false,
2276    }
2277}
2278
2279fn is_sign(op: &UnaryOperator) -> bool {
2280    matches!(op, UnaryOperator::Minus | UnaryOperator::Plus)
2281}
2282
2283/// Render an operand of a PostgreSQL postfix operator (`::`, `[]`, `COLLATE`, the
2284/// left of `AT TIME ZONE`) or the `AT TIME ZONE` zone, adding the binding-power
2285/// parentheses `parent`/`side` demand.
2286fn render_pg_operand<X: Extension + Render>(
2287    child: &Expr<X>,
2288    parent: BindingPower,
2289    side: Side,
2290    ctx: &RenderCtx<'_>,
2291    f: &mut fmt::Formatter<'_>,
2292) -> fmt::Result {
2293    let canonical = operand_needs_parens(&ctx.target().binding_powers, parent, child, side);
2294    render_operand(child, canonical, ctx, f)
2295}
2296
2297/// Whether `child` needs parentheses as an operand of an operator whose binding
2298/// power is `parent`, on the given `side`.
2299///
2300/// Centralizes child-precedence derivation for every operand position keyed by a
2301/// parent [`BindingPower`] — the PostgreSQL postfix operators and the extension
2302/// infix operators ([`render_extension_infix`]). A child binding looser than the
2303/// side the parent reaches across re-associates away without parentheses and must
2304/// be wrapped; a prefix-operator child closes its inner edge, so it only
2305/// needs parens as a looser-binding *left* operand. An extension operator child
2306/// reports its own precedence via [`Render::operand_binding_power`], so a custom
2307/// operator nested in any operand position groups by the same rule. Atoms, calls,
2308/// `CAST(...)` calls, the prefixed typed string constant (`type 'string'`), and the
2309/// self-delimiting `ARRAY`/`ROW`/`[…]`/`{…}`/`MAP {…}` constructors never need them.
2310fn operand_needs_parens<X: Extension + Render>(
2311    bp: &BindingPowerTable,
2312    parent: BindingPower,
2313    child: &Expr<X>,
2314    side: Side,
2315) -> bool {
2316    let child_bp = match child {
2317        Expr::BinaryOp { op, .. } => bp.binary(op),
2318        Expr::UnaryOp { op, .. } => {
2319            return side == Side::Left && bp.prefix(op) < parent.left;
2320        }
2321        Expr::Between { .. }
2322        | Expr::Like { .. }
2323        | Expr::QuantifiedLike { .. }
2324        | Expr::InList { .. }
2325        | Expr::InSubquery { .. } => bp.range_predicate(),
2326        Expr::IsNull { .. } | Expr::IsTruth { .. } | Expr::IsNormalized { .. } => bp.predicate(),
2327        // A quantified comparison binds at its comparison operator's rank (see
2328        // `binary_child_needs_parens`).
2329        Expr::QuantifiedComparison { .. } => bp.comparison,
2330        Expr::InExpr { .. } => UNPARENTHESIZED_IN_LIST,
2331        Expr::Cast {
2332            syntax: CastSyntax::DoubleColon,
2333            ..
2334        } => bp.typecast,
2335        Expr::Subscript { .. } => bp.subscript,
2336        Expr::Collate { .. } => bp.collate,
2337        Expr::AtTimeZone { .. } => bp.at_time_zone,
2338        Expr::FieldSelection { .. } => bp.field_selection,
2339        // A named operator (bare `a ~ b` or `OPERATOR(...)`) binds at the "any other
2340        // operator" rank.
2341        Expr::NamedOperator { .. } => bp.any_operator,
2342        // A prefix symbolic operator closes its inner edge, so — like a unary op — it only
2343        // needs parens as a looser-binding left operand.
2344        Expr::PrefixOperator { .. } => {
2345            return side == Side::Left && bp.any_operator.left < parent.left;
2346        }
2347        // A postfix symbolic operator closes its outer-left edge with its operand (the mirror
2348        // of a prefix operator), so it only needs parens as a looser-binding right operand.
2349        Expr::PostfixOperator { .. } => {
2350            return side == Side::Right && bp.any_operator.left < parent.right;
2351        }
2352        // A lambda binds at the JSON-arrow rank (it is that token).
2353        Expr::Lambda { .. } => bp.binary(&BinaryOperator::JsonGet),
2354        Expr::Other { ext, .. } => match ext.operand_binding_power() {
2355            Some(child_bp) => child_bp,
2356            None => return false,
2357        },
2358        _ => return false,
2359    };
2360    needs_parens_between(parent, child_bp, side)
2361}
2362
2363// ---------------------------------------------------------------------------
2364// Extension operator rendering (the public ADR-0009 seam)
2365// ---------------------------------------------------------------------------
2366
2367/// Render a binary extension operator — `operands.0`, the operator token written by
2368/// `op`, then `operands.1` — with exactly the parentheses its binding power `bp`
2369/// requires, plus the full self-wrapping the `Parenthesized` oracle mode adds.
2370///
2371/// This is the blessed way an `Expr::Other` infix-operator node renders: it reuses
2372/// the same machinery the built-in [`Expr::BinaryOp`]
2373/// arm uses, so a custom-operator tree round-trips by the same binding-power rule.
2374/// `bp` MUST equal the binding power the dialect's
2375/// `peek_infix_operator_hook` reported and that the node returns from
2376/// [`Render::operand_binding_power`]. `op` writes the operator token with its own
2377/// surrounding spaces (e.g. `|f| f.write_str(" ~ ")`). `operands` is the `(left,
2378/// right)` pair, bundled so the signature stays within the argument budget.
2379pub fn render_extension_infix<X: Extension + Render>(
2380    ctx: &RenderCtx<'_>,
2381    f: &mut fmt::Formatter<'_>,
2382    bp: BindingPower,
2383    operands: (&Expr<X>, &Expr<X>),
2384    op: impl FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result,
2385) -> fmt::Result {
2386    let (left, right) = operands;
2387    let table = &ctx.target().binding_powers;
2388    let full = ctx.mode() == RenderMode::Parenthesized;
2389    open_group(full, f)?;
2390    render_operand(
2391        left,
2392        operand_needs_parens(table, bp, left, Side::Left),
2393        ctx,
2394        f,
2395    )?;
2396    op(f)?;
2397    render_operand(
2398        right,
2399        operand_needs_parens(table, bp, right, Side::Right),
2400        ctx,
2401        f,
2402    )?;
2403    close_group(full, f)
2404}
2405
2406/// Render a prefix extension operator — the operator token written by `op`, then
2407/// `operand` — with exactly the parentheses its prefix binding power `bp` requires,
2408/// plus the `Parenthesized` self-wrapping.
2409///
2410/// The prefix analogue of [`render_extension_infix`]; `bp` MUST equal the binding
2411/// power the dialect's `peek_prefix_operator_hook` reported.
2412pub fn render_extension_prefix<X: Extension + Render>(
2413    ctx: &RenderCtx<'_>,
2414    f: &mut fmt::Formatter<'_>,
2415    bp: u8,
2416    op: impl FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result,
2417    operand: &Expr<X>,
2418) -> fmt::Result {
2419    let full = ctx.mode() == RenderMode::Parenthesized;
2420    open_group(full, f)?;
2421    op(f)?;
2422    render_operand(
2423        operand,
2424        prefix_extension_operand_needs_parens(&ctx.target().binding_powers, bp, operand),
2425        ctx,
2426        f,
2427    )?;
2428    close_group(full, f)
2429}
2430
2431/// Render a postfix extension operator — `operand`, then the operator token written by
2432/// `op` — with exactly the parentheses its binding power `bp` requires, plus the
2433/// `Parenthesized` self-wrapping.
2434///
2435/// The postfix analogue of [`render_extension_prefix`]; `bp` MUST equal the operator's
2436/// "any other operator" binding power. The operand is a *left* operand at that rank — a
2437/// postfix operator is a complete unary token, so its operand groups exactly as the left
2438/// operand of a same-rank infix operator (`operand_needs_parens(..., Side::Left)`): a
2439/// tighter operand needs no parens (`a + b !` re-parses `(a + b)!`), a looser one is
2440/// wrapped (`(a = b)!`).
2441pub fn render_extension_postfix<X: Extension + Render>(
2442    ctx: &RenderCtx<'_>,
2443    f: &mut fmt::Formatter<'_>,
2444    bp: BindingPower,
2445    operand: &Expr<X>,
2446    op: impl FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result,
2447) -> fmt::Result {
2448    let table = &ctx.target().binding_powers;
2449    let full = ctx.mode() == RenderMode::Parenthesized;
2450    open_group(full, f)?;
2451    render_operand(
2452        operand,
2453        operand_needs_parens(table, bp, operand, Side::Left),
2454        ctx,
2455        f,
2456    )?;
2457    op(f)?;
2458    close_group(full, f)
2459}
2460
2461/// Whether a prefix extension operator's operand needs parens, given the operator's
2462/// prefix binding power `bp`. A binary or comparison-level-predicate operand binding
2463/// looser than the prefix is wrapped; an extension operand reports its own
2464/// precedence; atoms and a nested unary operand (which closes its own edge) never
2465/// need them.
2466fn prefix_extension_operand_needs_parens<X: Extension + Render>(
2467    table: &BindingPowerTable,
2468    bp: u8,
2469    operand: &Expr<X>,
2470) -> bool {
2471    match operand {
2472        Expr::BinaryOp { op, .. } => table.binary(op).left < bp,
2473        Expr::IsNull { .. } | Expr::IsTruth { .. } | Expr::IsNormalized { .. } => {
2474            table.predicate().left < bp
2475        }
2476        // The range/pattern/membership family binds at the range-predicate rank; a quantified
2477        // comparison binds at its comparison operator's rank (parse-aligned, ADR-0008).
2478        Expr::Between { .. }
2479        | Expr::Like { .. }
2480        | Expr::QuantifiedLike { .. }
2481        | Expr::InList { .. }
2482        | Expr::InSubquery { .. } => table.range_predicate().left < bp,
2483        Expr::QuantifiedComparison { .. } => table.comparison.left < bp,
2484        Expr::InExpr { .. } => UNPARENTHESIZED_IN_LIST.left < bp,
2485        Expr::NamedOperator { .. } => table.any_operator.left < bp,
2486        Expr::PrefixOperator { .. } => table.any_operator.left < bp,
2487        Expr::PostfixOperator { .. } => table.any_operator.left < bp,
2488        // A lambda binds at the JSON-arrow rank, so a tighter prefix wraps it.
2489        Expr::Lambda { .. } => table.binary(&BinaryOperator::JsonGet).left < bp,
2490        Expr::Other { ext, .. } => ext
2491            .operand_binding_power()
2492            .is_some_and(|child| child.left < bp),
2493        _ => false,
2494    }
2495}
2496
2497// ---------------------------------------------------------------------------
2498// Statements and queries
2499// ---------------------------------------------------------------------------
2500
2501impl<X: Extension + Render> Render for Statement<X> {
2502    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2503        match self {
2504            Statement::Query { query, .. } => query.render(ctx, f),
2505            Statement::CreateTable { create, .. } => create.render(ctx, f),
2506            Statement::AlterTable { alter, .. } => alter.render(ctx, f),
2507            Statement::Drop { drop, .. } => drop.render(ctx, f),
2508            Statement::CreateSchema { schema, .. } => schema.render(ctx, f),
2509            Statement::CreateView { view, .. } => view.render(ctx, f),
2510            Statement::AlterView { alter, .. } => alter.render(ctx, f),
2511            Statement::CreateIndex { index, .. } => index.render(ctx, f),
2512            Statement::CreateFunction { create, .. } => create.render(ctx, f),
2513            Statement::CreateProcedure { create, .. } => create.render(ctx, f),
2514            Statement::AlterRoutine { alter, .. } => alter.render(ctx, f),
2515            Statement::CreateEvent { create, .. } => create.render(ctx, f),
2516            Statement::AlterEvent { alter, .. } => alter.render(ctx, f),
2517            Statement::DropEvent { drop, .. } => drop.render(ctx, f),
2518            Statement::DropDatabase { drop, .. } => drop.render(ctx, f),
2519            Statement::DropIndex { drop, .. } => drop.render(ctx, f),
2520            Statement::CreateDatabase { create, .. } => create.render(ctx, f),
2521            Statement::DropRoutine {
2522                kind,
2523                if_exists,
2524                routines,
2525                behavior,
2526                ..
2527            } => {
2528                f.write_str("DROP ")?;
2529                kind.render(ctx, f)?;
2530                if *if_exists {
2531                    f.write_str(" IF EXISTS")?;
2532                }
2533                f.write_str(" ")?;
2534                render_comma_separated(routines, ctx, f)?;
2535                render_drop_behavior(*behavior, ctx, f)
2536            }
2537            Statement::DropTransform { drop, .. } => drop.render(ctx, f),
2538            Statement::Truncate {
2539                tables,
2540                table_keyword,
2541                restart_identity,
2542                behavior,
2543                ..
2544            } => {
2545                // The optional `TABLE` keyword is exact-synonym sugar: the canonical form
2546                // always emits it, a source-fidelity render drops it when the source did.
2547                if *table_keyword || !honours_source_spelling(ctx) {
2548                    f.write_str("TRUNCATE TABLE ")?;
2549                } else {
2550                    f.write_str("TRUNCATE ")?;
2551                }
2552                render_comma_separated(tables, ctx, f)?;
2553                match restart_identity {
2554                    Some(true) => f.write_str(" RESTART IDENTITY")?,
2555                    Some(false) => f.write_str(" CONTINUE IDENTITY")?,
2556                    None => {}
2557                }
2558                render_drop_behavior(*behavior, ctx, f)
2559            }
2560            Statement::CommentOn { comment, .. } => {
2561                let CommentOnStatement {
2562                    target,
2563                    name,
2564                    comment,
2565                    ..
2566                } = comment.as_ref();
2567                f.write_str("COMMENT ON ")?;
2568                f.write_str(match target {
2569                    CommentTarget::Table => "TABLE ",
2570                    CommentTarget::Column => "COLUMN ",
2571                    CommentTarget::Database => "DATABASE ",
2572                    CommentTarget::Procedure { .. } => "PROCEDURE ",
2573                })?;
2574                name.render(ctx, f)?;
2575                if let CommentTarget::Procedure {
2576                    arg_types: Some(arg_types),
2577                } = target
2578                {
2579                    f.write_str("(")?;
2580                    render_comma_separated(arg_types, ctx, f)?;
2581                    f.write_str(")")?;
2582                }
2583                f.write_str(" IS ")?;
2584                match comment {
2585                    Some(literal) => literal.render(ctx, f)?,
2586                    None => f.write_str("NULL")?,
2587                }
2588                Ok(())
2589            }
2590            Statement::Insert { insert, .. } => insert.render(ctx, f),
2591            Statement::Update { update, .. } => update.render(ctx, f),
2592            Statement::Delete { delete, .. } => delete.render(ctx, f),
2593            Statement::Merge { merge, .. } => merge.render(ctx, f),
2594            Statement::Transaction { transaction, .. } => transaction.render(ctx, f),
2595            Statement::Xa { xa, .. } => xa.render(ctx, f),
2596            Statement::Session { session, .. } => session.render(ctx, f),
2597            Statement::AccessControl { access, .. } => access.render(ctx, f),
2598            Statement::Copy { copy, .. } => copy.render(ctx, f),
2599            Statement::CopyInto { copy, .. } => copy.render(ctx, f),
2600            Statement::Export { export, .. } => export.render(ctx, f),
2601            Statement::Import { import, .. } => import.render(ctx, f),
2602            Statement::Explain { explain, .. } => explain.render(ctx, f),
2603            Statement::Describe { describe, .. } => describe.render(ctx, f),
2604            Statement::Show { show, .. } => show.render(ctx, f),
2605            Statement::Kill { kill, .. } => kill.render(ctx, f),
2606            Statement::Handler { handler, .. } => handler.render(ctx, f),
2607            Statement::Install { install, .. } => install.render(ctx, f),
2608            Statement::Uninstall { uninstall, .. } => uninstall.render(ctx, f),
2609            Statement::Shutdown { .. } => f.write_str("SHUTDOWN"),
2610            Statement::Restart { .. } => f.write_str("RESTART"),
2611            Statement::Clone { clone, .. } => clone.render(ctx, f),
2612            Statement::ImportTable { import_table, .. } => import_table.render(ctx, f),
2613            Statement::Help { help, .. } => help.render(ctx, f),
2614            Statement::Binlog { binlog, .. } => binlog.render(ctx, f),
2615            Statement::Pragma { pragma, .. } => pragma.render(ctx, f),
2616            Statement::Attach { attach, .. } => attach.render(ctx, f),
2617            Statement::Detach { detach, .. } => detach.render(ctx, f),
2618            Statement::Checkpoint { checkpoint, .. } => checkpoint.render(ctx, f),
2619            Statement::Load { load, .. } => load.render(ctx, f),
2620            Statement::LoadData { load_data, .. } => load_data.render(ctx, f),
2621            Statement::UpdateExtensions {
2622                update_extensions, ..
2623            } => update_extensions.render(ctx, f),
2624            Statement::Vacuum { vacuum, .. } => vacuum.render(ctx, f),
2625            Statement::Reindex { reindex, .. } => reindex.render(ctx, f),
2626            Statement::Analyze { analyze, .. } => analyze.render(ctx, f),
2627            Statement::TableMaintenance {
2628                table_maintenance, ..
2629            } => table_maintenance.render(ctx, f),
2630            Statement::CacheIndex { cache_index, .. } => cache_index.render(ctx, f),
2631            Statement::LoadIndex { load_index, .. } => load_index.render(ctx, f),
2632            Statement::Rename { rename, .. } => rename.render(ctx, f),
2633            Statement::Flush { flush, .. } => flush.render(ctx, f),
2634            Statement::Purge { purge, .. } => purge.render(ctx, f),
2635            Statement::Replication { replication, .. } => replication.render(ctx, f),
2636            Statement::CreateUser { create, .. } => create.render(ctx, f),
2637            Statement::AlterUser { alter, .. } => alter.render(ctx, f),
2638            Statement::UserRoleList { statement, .. } => statement.render(ctx, f),
2639            Statement::Use { use_statement, .. } => use_statement.render(ctx, f),
2640            Statement::CreateTrigger { create, .. } => create.render(ctx, f),
2641            Statement::CreateStoredTrigger { create, .. } => create.render(ctx, f),
2642            Statement::CreateMacro { create, .. } => create.render(ctx, f),
2643            Statement::CreateSecret { create, .. } => create.render(ctx, f),
2644            Statement::DropSecret { drop, .. } => drop.render(ctx, f),
2645            Statement::CreateType { create, .. } => create.render(ctx, f),
2646            Statement::CreateVirtualTable { create, .. } => create.render(ctx, f),
2647            Statement::CreateSequence { create, .. } => create.render(ctx, f),
2648            Statement::CreateExtension { create, .. } => create.render(ctx, f),
2649            Statement::AlterExtension { alter, .. } => alter.render(ctx, f),
2650            Statement::CreateTablespace { create, .. } => create.render(ctx, f),
2651            Statement::AlterTablespace { alter, .. } => alter.render(ctx, f),
2652            Statement::DropTablespace { drop, .. } => drop.render(ctx, f),
2653            Statement::CreateLogfileGroup { create, .. } => create.render(ctx, f),
2654            Statement::AlterLogfileGroup { alter, .. } => alter.render(ctx, f),
2655            Statement::DropLogfileGroup { drop, .. } => drop.render(ctx, f),
2656            Statement::AlterObjectDepends { alter, .. } => alter.render(ctx, f),
2657            Statement::AlterSystem { alter, .. } => alter.render(ctx, f),
2658            Statement::AlterDatabase { alter, .. } => alter.render(ctx, f),
2659            Statement::AlterDatabaseOptions { alter, .. } => alter.render(ctx, f),
2660            Statement::CreateServer { create, .. } => create.render(ctx, f),
2661            Statement::AlterServer { alter, .. } => alter.render(ctx, f),
2662            Statement::DropServer { drop, .. } => drop.render(ctx, f),
2663            Statement::AlterInstance { alter, .. } => alter.render(ctx, f),
2664            Statement::CreateSpatialReferenceSystem { create, .. } => create.render(ctx, f),
2665            Statement::DropSpatialReferenceSystem { drop, .. } => drop.render(ctx, f),
2666            Statement::CreateResourceGroup { create, .. } => create.render(ctx, f),
2667            Statement::AlterResourceGroup { alter, .. } => alter.render(ctx, f),
2668            Statement::DropResourceGroup { drop, .. } => drop.render(ctx, f),
2669            Statement::AlterSequence { alter, .. } => alter.render(ctx, f),
2670            Statement::AlterObjectSchema { alter, .. } => alter.render(ctx, f),
2671            Statement::Pivot { pivot, .. } => pivot.render(ctx, f),
2672            Statement::Unpivot { unpivot, .. } => unpivot.render(ctx, f),
2673            // The statement form is the bare `SHOW_REF` (`DESCRIBE …` / `SUMMARIZE …`),
2674            // unparenthesized — the `ShowRef` core already renders the keyword + target; the
2675            // parentheses are the table-factor position's alone.
2676            Statement::ShowRef { show, .. } => show.render(ctx, f),
2677            Statement::Prepare { prepare, .. } => prepare.render(ctx, f),
2678            Statement::Execute { execute, .. } => execute.render(ctx, f),
2679            Statement::PrepareFrom { prepare_from, .. } => prepare_from.render(ctx, f),
2680            Statement::ExecuteUsing { execute_using, .. } => execute_using.render(ctx, f),
2681            Statement::Deallocate { deallocate, .. } => deallocate.render(ctx, f),
2682            Statement::Call { call, .. } => call.render(ctx, f),
2683            Statement::Do { do_block, .. } => do_block.render(ctx, f),
2684            Statement::DoExpressions { do_expressions, .. } => do_expressions.render(ctx, f),
2685            Statement::LockTables { lock_tables, .. } => lock_tables.render(ctx, f),
2686            Statement::UnlockTables { unlock_tables, .. } => unlock_tables.render(ctx, f),
2687            Statement::InstanceLock { instance_lock, .. } => instance_lock.render(ctx, f),
2688            Statement::Compound { compound, .. } => compound.render(ctx, f),
2689            Statement::If { if_statement, .. } => if_statement.render(ctx, f),
2690            Statement::Case { case_statement, .. } => case_statement.render(ctx, f),
2691            Statement::Loop { loop_statement, .. } => loop_statement.render(ctx, f),
2692            Statement::While {
2693                while_statement, ..
2694            } => while_statement.render(ctx, f),
2695            Statement::Repeat { repeat, .. } => repeat.render(ctx, f),
2696            Statement::Leave { leave, .. } => leave.render(ctx, f),
2697            Statement::Iterate { iterate, .. } => iterate.render(ctx, f),
2698            Statement::Return {
2699                return_statement, ..
2700            } => return_statement.render(ctx, f),
2701            Statement::OpenCursor { open, .. } => open.render(ctx, f),
2702            Statement::FetchCursor { fetch, .. } => fetch.render(ctx, f),
2703            Statement::CloseCursor { close, .. } => close.render(ctx, f),
2704            Statement::Signal { signal, .. } => signal.render_as(ctx, f, "SIGNAL"),
2705            Statement::Resignal { resignal, .. } => resignal.render_as(ctx, f, "RESIGNAL"),
2706            Statement::GetDiagnostics {
2707                get_diagnostics, ..
2708            } => get_diagnostics.render(ctx, f),
2709            Statement::Other { ext, .. } => ext.render(ctx, f),
2710        }
2711    }
2712}
2713
2714impl<X: Extension + Render> Render for Insert<X> {
2715    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2716        if let Some(with) = &self.with {
2717            with.render(ctx, f)?;
2718            f.write_str(" ")?;
2719        }
2720        // `INTO` is mandatory after `INSERT` and rendered canonically after `REPLACE`
2721        // too (the MySQL shorthand `REPLACE t` parses but normalizes to `REPLACE INTO`,
2722        // matching how a bare DML alias normalizes to the `AS` spelling). The SQLite
2723        // `OR <action>` conflict prefix sits between the verb keyword and `INTO`; the
2724        // parser only sets it on the `INSERT` spelling (`REPLACE` takes none).
2725        f.write_str(match self.verb {
2726            InsertVerb::Insert => "INSERT",
2727            InsertVerb::Replace => "REPLACE",
2728        })?;
2729        if let Some(or_action) = self.or_action {
2730            f.write_str(" OR ")?;
2731            or_action.render(ctx, f)?;
2732        }
2733        f.write_str(" INTO ")?;
2734        self.target.render(ctx, f)?;
2735        if let Some(mode) = self.column_matching {
2736            f.write_str(match mode {
2737                InsertColumnMatching::ByName => " BY NAME",
2738                InsertColumnMatching::ByPosition => " BY POSITION",
2739            })?;
2740        }
2741        if let Some(overriding) = self.overriding {
2742            f.write_str(" ")?;
2743            overriding.render(ctx, f)?;
2744        }
2745        f.write_str(" ")?;
2746        self.source.render(ctx, f)?;
2747        // The MySQL row alias sits between the source and the `ON DUPLICATE KEY UPDATE`
2748        // clause; `render_table_alias` writes the leading ` AS `.
2749        render_table_alias(self.row_alias.as_ref(), ctx, f)?;
2750        if let Some(upsert) = &self.upsert {
2751            f.write_str(" ")?;
2752            upsert.render(ctx, f)?;
2753        }
2754        render_returning_clause(self.returning.as_ref(), ctx, f)
2755    }
2756}
2757
2758impl Render for InsertTarget {
2759    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2760        self.name.render(ctx, f)?;
2761        render_alias(self.alias.as_ref(), self.alias_spelling, ctx, f)?;
2762        if !self.columns.is_empty() {
2763            f.write_str(" (")?;
2764            render_ident_list(&self.columns, ctx, f)?;
2765            f.write_str(")")?;
2766        }
2767        Ok(())
2768    }
2769}
2770
2771impl Render for InsertOverriding {
2772    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2773        f.write_str(match self {
2774            Self::SystemValue => "OVERRIDING SYSTEM VALUE",
2775            Self::UserValue => "OVERRIDING USER VALUE",
2776        })
2777    }
2778}
2779
2780impl Render for ConflictResolution {
2781    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2782        f.write_str(match self {
2783            Self::Rollback => "ROLLBACK",
2784            Self::Abort => "ABORT",
2785            Self::Fail => "FAIL",
2786            Self::Ignore => "IGNORE",
2787            Self::Replace => "REPLACE",
2788        })
2789    }
2790}
2791
2792impl<X: Extension + Render> Render for InsertSource<X> {
2793    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2794        match self {
2795            Self::DefaultValues { default, .. } => {
2796                default.render(ctx, f)?;
2797                f.write_str(" VALUES")
2798            }
2799            Self::Values { values, .. } => values.render(ctx, f),
2800            Self::Query { query, .. } => query.render(ctx, f),
2801            Self::Set { assignments, .. } => {
2802                f.write_str("SET ")?;
2803                render_update_assignments(assignments, ctx, f)
2804            }
2805        }
2806    }
2807}
2808
2809impl<X: Extension + Render> Render for InsertValues<X> {
2810    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2811        // The `INSERT ... VALUES` source list uses bare `( ... )` rows on every dialect.
2812        f.write_str("VALUES ")?;
2813        render_values_rows(&self.rows, false, ctx, f)
2814    }
2815}
2816
2817impl<X: Extension + Render> Render for InsertValue<X> {
2818    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2819        match self {
2820            Self::Expr { expr, .. } => expr.render(ctx, f),
2821            Self::Default { default, .. } => default.render(ctx, f),
2822        }
2823    }
2824}
2825
2826impl Render for DefaultValue {
2827    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2828        f.write_str("DEFAULT")
2829    }
2830}
2831
2832impl Render for DmlTarget {
2833    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2834        render_relation_inheritance(&self.inheritance, &self.name, ctx, f)?;
2835        render_alias(self.alias.as_ref(), self.alias_spelling, ctx, f)
2836    }
2837}
2838
2839impl<X: Extension + Render> Render for Update<X> {
2840    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2841        if let Some(with) = &self.with {
2842            with.render(ctx, f)?;
2843            f.write_str(" ")?;
2844        }
2845        f.write_str("UPDATE")?;
2846        if let Some(or_action) = self.or_action {
2847            f.write_str(" OR ")?;
2848            or_action.render(ctx, f)?;
2849        }
2850        f.write_str(" ")?;
2851        self.target.render(ctx, f)?;
2852        f.write_str(" SET ")?;
2853        render_update_assignments(&self.assignments, ctx, f)?;
2854        if !self.from.is_empty() {
2855            f.write_str(" FROM ")?;
2856            render_comma_separated(&self.from, ctx, f)?;
2857        }
2858        render_dml_selection(self.selection.as_ref(), ctx, f)?;
2859        render_mutation_order_by_limit(&self.order_by, self.limit.as_ref(), ctx, f)?;
2860        render_returning_clause(self.returning.as_ref(), ctx, f)
2861    }
2862}
2863
2864impl<X: Extension + Render> Render for UpdateAssignment<X> {
2865    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2866        match self {
2867            Self::Single { target, value, .. } => {
2868                target.render(ctx, f)?;
2869                f.write_str(" = ")?;
2870                value.render(ctx, f)
2871            }
2872            Self::Tuple {
2873                targets, source, ..
2874            } => {
2875                f.write_str("(")?;
2876                render_comma_separated(targets, ctx, f)?;
2877                f.write_str(") = ")?;
2878                source.render(ctx, f)
2879            }
2880        }
2881    }
2882}
2883
2884impl<X: Extension + Render> Render for UpdateValue<X> {
2885    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2886        match self {
2887            Self::Expr { expr, .. } => expr.render(ctx, f),
2888            Self::Default { default, .. } => default.render(ctx, f),
2889        }
2890    }
2891}
2892
2893impl<X: Extension + Render> Render for UpdateTupleSource<X> {
2894    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2895        match self {
2896            Self::Row {
2897                explicit, values, ..
2898            } => {
2899                f.write_str(if *explicit { "ROW(" } else { "(" })?;
2900                render_comma_separated(values, ctx, f)?;
2901                f.write_str(")")
2902            }
2903            Self::Subquery { query, .. } => {
2904                f.write_str("(")?;
2905                query.render(ctx, f)?;
2906                f.write_str(")")
2907            }
2908            Self::Default { default, .. } => default.render(ctx, f),
2909        }
2910    }
2911}
2912
2913impl<X: Extension + Render> Render for Delete<X> {
2914    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2915        if let Some(with) = &self.with {
2916            with.render(ctx, f)?;
2917            f.write_str(" ")?;
2918        }
2919        f.write_str("DELETE FROM ")?;
2920        self.target.render(ctx, f)?;
2921        if !self.using.is_empty() {
2922            f.write_str(" USING ")?;
2923            render_comma_separated(&self.using, ctx, f)?;
2924        }
2925        render_dml_selection(self.selection.as_ref(), ctx, f)?;
2926        render_mutation_order_by_limit(&self.order_by, self.limit.as_ref(), ctx, f)?;
2927        render_returning_clause(self.returning.as_ref(), ctx, f)
2928    }
2929}
2930
2931/// Render a trailing ` WHERE ...` filter when present; the leading space is part of
2932/// the clause separator, mirroring the other mutation tails.
2933/// Render a non-empty comma-separated `UpdateAssignment` list — the shared `SET`
2934/// body of an `UPDATE`, a PostgreSQL `ON CONFLICT DO UPDATE`, and a MySQL
2935/// `ON DUPLICATE KEY UPDATE`. The `SET` keyword (and any clause lead-in) is the
2936/// caller's, so the three spellings differ only in their prefix, not this body.
2937fn render_update_assignments<X: Extension + Render>(
2938    assignments: &[UpdateAssignment<X>],
2939    ctx: &RenderCtx<'_>,
2940    f: &mut fmt::Formatter<'_>,
2941) -> fmt::Result {
2942    for (i, assignment) in assignments.iter().enumerate() {
2943        if i > 0 {
2944            f.write_str(", ")?;
2945        }
2946        assignment.render(ctx, f)?;
2947    }
2948    Ok(())
2949}
2950
2951fn render_dml_selection<X: Extension + Render>(
2952    selection: Option<&DmlSelection<X>>,
2953    ctx: &RenderCtx<'_>,
2954    f: &mut fmt::Formatter<'_>,
2955) -> fmt::Result {
2956    if let Some(selection) = selection {
2957        f.write_str(" WHERE ")?;
2958        selection.render(ctx, f)?;
2959    }
2960    Ok(())
2961}
2962
2963impl<X: Extension + Render> Render for DmlSelection<X> {
2964    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2965        match self {
2966            Self::Where { condition, .. } => condition.render(ctx, f),
2967            Self::CurrentOf { cursor, .. } => {
2968                f.write_str("CURRENT OF ")?;
2969                cursor.render(ctx, f)
2970            }
2971        }
2972    }
2973}
2974
2975/// Render the MySQL `[ORDER BY <keys>] [LIMIT <count>]` row-limiting tails shared by a
2976/// single-table `UPDATE` and `DELETE`; each leading space is part of the clause
2977/// separator, mirroring [`Query`]'s `ORDER BY` / `LIMIT` rendering.
2978fn render_mutation_order_by_limit<X: Extension + Render>(
2979    order_by: &[OrderByExpr<X>],
2980    limit: Option<&Limit<X>>,
2981    ctx: &RenderCtx<'_>,
2982    f: &mut fmt::Formatter<'_>,
2983) -> fmt::Result {
2984    if !order_by.is_empty() {
2985        f.write_str(" ORDER BY ")?;
2986        render_comma_separated(order_by, ctx, f)?;
2987    }
2988    if let Some(limit) = limit {
2989        f.write_str(" ")?;
2990        limit.render(ctx, f)?;
2991    }
2992    Ok(())
2993}
2994
2995/// Render a trailing ` RETURNING <output> [, ...]` clause when present; the leading
2996/// space is part of the clause separator, mirroring the other mutation tails.
2997fn render_returning_clause<X: Extension + Render>(
2998    returning: Option<&Returning<X>>,
2999    ctx: &RenderCtx<'_>,
3000    f: &mut fmt::Formatter<'_>,
3001) -> fmt::Result {
3002    if let Some(returning) = returning {
3003        f.write_str(" ")?;
3004        returning.render(ctx, f)?;
3005    }
3006    Ok(())
3007}
3008
3009impl<X: Extension + Render> Render for Returning<X> {
3010    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3011        f.write_str("RETURNING ")?;
3012        render_comma_separated(&self.items, ctx, f)
3013    }
3014}
3015
3016impl<X: Extension + Render> Render for Upsert<X> {
3017    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3018        match self {
3019            Upsert::OnConflict { conflict, .. } => conflict.render(ctx, f),
3020            Upsert::OnDuplicateKeyUpdate { assignments, .. } => {
3021                f.write_str("ON DUPLICATE KEY UPDATE ")?;
3022                render_update_assignments(assignments, ctx, f)
3023            }
3024        }
3025    }
3026}
3027
3028impl<X: Extension + Render> Render for OnConflict<X> {
3029    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3030        f.write_str("ON CONFLICT")?;
3031        if let Some(target) = &self.target {
3032            f.write_str(" ")?;
3033            target.render(ctx, f)?;
3034        }
3035        f.write_str(" ")?;
3036        self.action.render(ctx, f)
3037    }
3038}
3039
3040impl<X: Extension + Render> Render for ConflictTarget<X> {
3041    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3042        match self {
3043            ConflictTarget::Index {
3044                columns, predicate, ..
3045            } => {
3046                f.write_str("(")?;
3047                render_comma_separated(columns, ctx, f)?;
3048                f.write_str(")")?;
3049                if let Some(predicate) = predicate {
3050                    f.write_str(" WHERE ")?;
3051                    predicate.render(ctx, f)?;
3052                }
3053                Ok(())
3054            }
3055            ConflictTarget::Constraint { name, .. } => {
3056                f.write_str("ON CONSTRAINT ")?;
3057                name.render(ctx, f)
3058            }
3059        }
3060    }
3061}
3062
3063impl<X: Extension + Render> Render for ConflictAction<X> {
3064    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3065        match self {
3066            ConflictAction::Nothing { .. } => f.write_str("DO NOTHING"),
3067            ConflictAction::Update {
3068                assignments,
3069                selection,
3070                ..
3071            } => {
3072                f.write_str("DO UPDATE SET ")?;
3073                render_update_assignments(assignments, ctx, f)?;
3074                if let Some(selection) = selection {
3075                    f.write_str(" WHERE ")?;
3076                    selection.render(ctx, f)?;
3077                }
3078                Ok(())
3079            }
3080        }
3081    }
3082}
3083
3084impl<X: Extension + Render> Render for Merge<X> {
3085    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3086        if let Some(with) = &self.with {
3087            with.render(ctx, f)?;
3088            f.write_str(" ")?;
3089        }
3090        f.write_str("MERGE INTO ")?;
3091        self.target.render(ctx, f)?;
3092        f.write_str(" USING ")?;
3093        self.using.render(ctx, f)?;
3094        f.write_str(" ON ")?;
3095        self.on.render(ctx, f)?;
3096        for clause in &self.clauses {
3097            f.write_str(" ")?;
3098            clause.render(ctx, f)?;
3099        }
3100        render_returning_clause(self.returning.as_ref(), ctx, f)
3101    }
3102}
3103
3104impl<X: Extension + Render> Render for MergeWhenClause<X> {
3105    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3106        // `NotMatchedByTarget` renders the canonical bare spelling — `BY TARGET` is
3107        // the same production (both fold at parse), so the qualifier only appears
3108        // where it is load-bearing (`BY SOURCE`).
3109        f.write_str(match self.match_kind {
3110            MergeMatchKind::Matched => "WHEN MATCHED",
3111            MergeMatchKind::NotMatchedByTarget => "WHEN NOT MATCHED",
3112            MergeMatchKind::NotMatchedBySource => "WHEN NOT MATCHED BY SOURCE",
3113        })?;
3114        if let Some(condition) = &self.condition {
3115            f.write_str(" AND ")?;
3116            condition.render(ctx, f)?;
3117        }
3118        f.write_str(" THEN ")?;
3119        self.action.render(ctx, f)
3120    }
3121}
3122
3123impl<X: Extension + Render> Render for MergeAction<X> {
3124    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3125        match self {
3126            MergeAction::Insert {
3127                columns,
3128                overriding,
3129                values,
3130                ..
3131            } => {
3132                f.write_str("INSERT")?;
3133                if !columns.is_empty() {
3134                    f.write_str(" (")?;
3135                    render_ident_list(columns, ctx, f)?;
3136                    f.write_str(")")?;
3137                }
3138                if let Some(overriding) = overriding {
3139                    f.write_str(" ")?;
3140                    overriding.render(ctx, f)?;
3141                }
3142                f.write_str(" VALUES (")?;
3143                render_comma_separated(values, ctx, f)?;
3144                f.write_str(")")
3145            }
3146            MergeAction::InsertDefault { default, .. } => {
3147                f.write_str("INSERT ")?;
3148                default.render(ctx, f)?;
3149                f.write_str(" VALUES")
3150            }
3151            MergeAction::Update { assignments, .. } => {
3152                f.write_str("UPDATE SET ")?;
3153                render_update_assignments(assignments, ctx, f)
3154            }
3155            MergeAction::UpdateStar { .. } => f.write_str("UPDATE SET *"),
3156            MergeAction::InsertStar { .. } => f.write_str("INSERT *"),
3157            MergeAction::InsertByName { star, .. } => {
3158                f.write_str("INSERT BY NAME")?;
3159                if *star {
3160                    f.write_str(" *")?;
3161                }
3162                Ok(())
3163            }
3164            MergeAction::Error { .. } => f.write_str("ERROR"),
3165            MergeAction::Delete { .. } => f.write_str("DELETE"),
3166            MergeAction::DoNothing { .. } => f.write_str("DO NOTHING"),
3167        }
3168    }
3169}
3170
3171impl Render for TransactionStatement {
3172    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3173        match self {
3174            Self::Begin {
3175                syntax,
3176                mode,
3177                block,
3178                modes,
3179                ..
3180            } => {
3181                f.write_str(match syntax {
3182                    TransactionStart::Begin => "BEGIN",
3183                    TransactionStart::Start => "START TRANSACTION",
3184                })?;
3185                if let Some(mode) = mode {
3186                    f.write_str(" ")?;
3187                    mode.render(ctx, f)?;
3188                }
3189                // The `TRANSACTION`/`WORK` block noise word is exact-synonym fidelity —
3190                // replayed only by a source-fidelity render, dropped by a re-spell and
3191                // the redacted fingerprint. Under `START TRANSACTION` the keyword is
3192                // part of the `syntax` string, so the tag never applies there.
3193                if *syntax == TransactionStart::Begin && honours_source_spelling(ctx) {
3194                    render_transaction_block_keyword(*block, f)?;
3195                }
3196                render_transaction_modes(modes, ctx, f)
3197            }
3198            Self::Commit { block, .. } => {
3199                f.write_str("COMMIT")?;
3200                if honours_source_spelling(ctx) {
3201                    render_transaction_block_keyword(*block, f)?;
3202                }
3203                Ok(())
3204            }
3205            Self::Rollback {
3206                block,
3207                savepoint_keyword,
3208                to_savepoint,
3209                ..
3210            } => {
3211                f.write_str("ROLLBACK")?;
3212                if honours_source_spelling(ctx) {
3213                    render_transaction_block_keyword(*block, f)?;
3214                }
3215                if let Some(name) = to_savepoint {
3216                    // The `SAVEPOINT` keyword is optional after `TO`; the canonical
3217                    // render emits it, a source-fidelity render drops it when the
3218                    // source did (`ROLLBACK TO x`).
3219                    if *savepoint_keyword || !honours_source_spelling(ctx) {
3220                        f.write_str(" TO SAVEPOINT ")?;
3221                    } else {
3222                        f.write_str(" TO ")?;
3223                    }
3224                    name.render(ctx, f)?;
3225                }
3226                Ok(())
3227            }
3228            Self::Savepoint { name, .. } => {
3229                f.write_str("SAVEPOINT ")?;
3230                name.render(ctx, f)
3231            }
3232            Self::Release {
3233                savepoint_keyword,
3234                savepoint,
3235                ..
3236            } => {
3237                // The `SAVEPOINT` keyword is optional; the canonical render emits it, a
3238                // source-fidelity render drops it when the source did (`RELEASE x`).
3239                if *savepoint_keyword || !honours_source_spelling(ctx) {
3240                    f.write_str("RELEASE SAVEPOINT ")?;
3241                } else {
3242                    f.write_str("RELEASE ")?;
3243                }
3244                savepoint.render(ctx, f)
3245            }
3246            Self::SetCharacteristics { modes, .. } => {
3247                f.write_str("SET TRANSACTION")?;
3248                render_transaction_modes(modes, ctx, f)
3249            }
3250        }
3251    }
3252}
3253
3254/// Render the optional `TRANSACTION` / `WORK` block noise word (with its leading
3255/// space) after `BEGIN` / `COMMIT` / `ROLLBACK`; a `None` tag renders nothing. Only a
3256/// source-fidelity render calls this — the callers already gate on
3257/// [`honours_source_spelling`].
3258fn render_transaction_block_keyword(
3259    block: Option<TransactionBlockKeyword>,
3260    f: &mut fmt::Formatter<'_>,
3261) -> fmt::Result {
3262    match block {
3263        Some(TransactionBlockKeyword::Transaction) => f.write_str(" TRANSACTION"),
3264        Some(TransactionBlockKeyword::Work) => f.write_str(" WORK"),
3265        None => Ok(()),
3266    }
3267}
3268
3269/// Render a transaction mode list, each mode preceded by its separator (a leading
3270/// space for the first, `, ` thereafter), so an empty list renders nothing.
3271fn render_transaction_modes(
3272    modes: &[TransactionMode],
3273    ctx: &RenderCtx<'_>,
3274    f: &mut fmt::Formatter<'_>,
3275) -> fmt::Result {
3276    render_leading_space_comma_separated(modes, ctx, f)
3277}
3278
3279impl Render for TransactionModeKind {
3280    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3281        f.write_str(match self {
3282            Self::Deferred => "DEFERRED",
3283            Self::Immediate => "IMMEDIATE",
3284            Self::Exclusive => "EXCLUSIVE",
3285        })
3286    }
3287}
3288
3289impl Render for TransactionMode {
3290    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3291        match self {
3292            Self::IsolationLevel { level, .. } => {
3293                f.write_str("ISOLATION LEVEL ")?;
3294                level.render(ctx, f)
3295            }
3296            Self::AccessMode { access, .. } => access.render(ctx, f),
3297            Self::Deferrable { deferrable, .. } => f.write_str(if *deferrable {
3298                "DEFERRABLE"
3299            } else {
3300                "NOT DEFERRABLE"
3301            }),
3302        }
3303    }
3304}
3305
3306impl Render for IsolationLevel {
3307    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3308        f.write_str(match self {
3309            Self::ReadUncommitted => "READ UNCOMMITTED",
3310            Self::ReadCommitted => "READ COMMITTED",
3311            Self::RepeatableRead => "REPEATABLE READ",
3312            Self::Serializable => "SERIALIZABLE",
3313        })
3314    }
3315}
3316
3317impl Render for TransactionAccessMode {
3318    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3319        f.write_str(match self {
3320            Self::ReadOnly => "READ ONLY",
3321            Self::ReadWrite => "READ WRITE",
3322        })
3323    }
3324}
3325
3326impl Render for XaStatement {
3327    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3328        match self {
3329            Self::Start {
3330                keyword,
3331                xid,
3332                association,
3333                ..
3334            } => {
3335                f.write_str(match keyword {
3336                    XaStartKeyword::Start => "XA START ",
3337                    XaStartKeyword::Begin => "XA BEGIN ",
3338                })?;
3339                xid.render(ctx, f)?;
3340                if let Some(association) = association {
3341                    f.write_str(match association {
3342                        XaAssociation::Join => " JOIN",
3343                        XaAssociation::Resume => " RESUME",
3344                    })?;
3345                }
3346                Ok(())
3347            }
3348            Self::End { xid, suspend, .. } => {
3349                f.write_str("XA END ")?;
3350                xid.render(ctx, f)?;
3351                if let Some(suspend) = suspend {
3352                    f.write_str(match suspend {
3353                        XaSuspend::Suspend => " SUSPEND",
3354                        XaSuspend::SuspendForMigrate => " SUSPEND FOR MIGRATE",
3355                    })?;
3356                }
3357                Ok(())
3358            }
3359            Self::Prepare { xid, .. } => {
3360                f.write_str("XA PREPARE ")?;
3361                xid.render(ctx, f)
3362            }
3363            Self::Commit { xid, one_phase, .. } => {
3364                f.write_str("XA COMMIT ")?;
3365                xid.render(ctx, f)?;
3366                if *one_phase {
3367                    f.write_str(" ONE PHASE")?;
3368                }
3369                Ok(())
3370            }
3371            Self::Rollback { xid, .. } => {
3372                f.write_str("XA ROLLBACK ")?;
3373                xid.render(ctx, f)
3374            }
3375            Self::Recover { convert_xid, .. } => {
3376                f.write_str("XA RECOVER")?;
3377                if *convert_xid {
3378                    f.write_str(" CONVERT XID")?;
3379                }
3380                Ok(())
3381            }
3382        }
3383    }
3384}
3385
3386impl Render for Xid {
3387    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3388        self.gtrid.render(ctx, f)?;
3389        if let Some(bqual) = &self.bqual {
3390            f.write_str(", ")?;
3391            bqual.render(ctx, f)?;
3392        }
3393        if let Some(format_id) = &self.format_id {
3394            f.write_str(", ")?;
3395            format_id.render(ctx, f)?;
3396        }
3397        Ok(())
3398    }
3399}
3400
3401impl<X: Extension + Render> Render for SessionStatement<X> {
3402    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3403        match self {
3404            Self::Set {
3405                scope,
3406                name,
3407                assignment,
3408                value,
3409                ..
3410            } => {
3411                f.write_str("SET")?;
3412                render_set_scope(*scope, ctx, f)?;
3413                f.write_str(" ")?;
3414                name.render(ctx, f)?;
3415                // `=` and `TO` are exact synonyms; a source-fidelity render replays the
3416                // written `=`, a target re-spell and the redacted fingerprint keep `TO`.
3417                f.write_str(match assignment {
3418                    SetAssignment::Equals if honours_source_spelling(ctx) => " = ",
3419                    _ => " TO ",
3420                })?;
3421                value.render(ctx, f)
3422            }
3423            Self::SetTimeZone { scope, value, .. } => {
3424                f.write_str("SET")?;
3425                render_set_scope(*scope, ctx, f)?;
3426                f.write_str(" TIME ZONE ")?;
3427                value.render(ctx, f)
3428            }
3429            Self::SetRole { scope, role, .. } => {
3430                f.write_str("SET")?;
3431                render_set_scope(*scope, ctx, f)?;
3432                f.write_str(" ROLE ")?;
3433                role.render(ctx, f)
3434            }
3435            Self::SetSessionAuthorization { scope, user, .. } => {
3436                f.write_str("SET")?;
3437                render_set_scope(*scope, ctx, f)?;
3438                f.write_str(" SESSION AUTHORIZATION ")?;
3439                user.render(ctx, f)
3440            }
3441            Self::SetConstraints {
3442                constraints,
3443                check_time,
3444                ..
3445            } => {
3446                f.write_str("SET CONSTRAINTS ")?;
3447                constraints.render(ctx, f)?;
3448                f.write_str(" ")?;
3449                check_time.render(ctx, f)
3450            }
3451            Self::SetNames { value, .. } => {
3452                f.write_str("SET NAMES ")?;
3453                value.render(ctx, f)
3454            }
3455            Self::SetSessionCharacteristics { modes, .. } => {
3456                f.write_str("SET SESSION CHARACTERISTICS AS TRANSACTION")?;
3457                render_transaction_modes(modes, ctx, f)
3458            }
3459            Self::Reset { scope, target, .. } => {
3460                f.write_str("RESET")?;
3461                render_set_scope(*scope, ctx, f)?;
3462                f.write_str(" ")?;
3463                target.render(ctx, f)
3464            }
3465            Self::Show {
3466                target, verbose, ..
3467            } => {
3468                f.write_str("SHOW ")?;
3469                target.render(ctx, f)?;
3470                if *verbose {
3471                    f.write_str(" VERBOSE")?;
3472                }
3473                Ok(())
3474            }
3475            Self::SetVariables { assignments, .. } => {
3476                f.write_str("SET ")?;
3477                render_comma_separated(assignments, ctx, f)
3478            }
3479            Self::SetCharacterSet { keyword, value, .. } => {
3480                f.write_str(match keyword {
3481                    CharacterSetKeyword::CharacterSet => "SET CHARACTER SET ",
3482                    CharacterSetKeyword::Charset => "SET CHARSET ",
3483                })?;
3484                value.render(ctx, f)
3485            }
3486            Self::SetResourceGroup {
3487                name, thread_ids, ..
3488            } => {
3489                f.write_str("SET RESOURCE GROUP ")?;
3490                name.render(ctx, f)?;
3491                if let Some(thread_ids) = thread_ids {
3492                    f.write_str(" FOR ")?;
3493                    render_comma_separated(thread_ids, ctx, f)?;
3494                }
3495                Ok(())
3496            }
3497        }
3498    }
3499}
3500
3501impl<X: Extension + Render> Render for SetVariableAssignment<X> {
3502    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3503        match self {
3504            Self::SystemVariable {
3505                scope,
3506                name,
3507                assignment,
3508                value,
3509                ..
3510            } => {
3511                scope.render_prefix(name, ctx, f)?;
3512                render_mysql_set_assignment(*assignment, ctx, f)?;
3513                value.render(ctx, f)
3514            }
3515            Self::UserVariable {
3516                name,
3517                assignment,
3518                value,
3519                ..
3520            } => {
3521                f.write_str("@")?;
3522                name.render(ctx, f)?;
3523                render_mysql_set_assignment(*assignment, ctx, f)?;
3524                value.render(ctx, f)
3525            }
3526        }
3527    }
3528}
3529
3530/// Render the `=` / `:=` separator of a MySQL variable assignment. The two are exact
3531/// synonyms; a source-fidelity render replays the written `:=`, a target re-spell and the
3532/// redacted fingerprint normalize to `=` (there is no `TO` spelling in MySQL's `SET`).
3533fn render_mysql_set_assignment(
3534    assignment: SetAssignment,
3535    ctx: &RenderCtx<'_>,
3536    f: &mut fmt::Formatter<'_>,
3537) -> fmt::Result {
3538    f.write_str(match assignment {
3539        SetAssignment::ColonEquals if honours_source_spelling(ctx) => " := ",
3540        _ => " = ",
3541    })
3542}
3543
3544impl SystemVariableScope {
3545    /// Render the scope prefix and the variable `name` together — a keyword prefix
3546    /// (`GLOBAL x`), the `@@` sigil (`@@x` / `@@global.x`), or the bare name.
3547    fn render_prefix(
3548        self,
3549        name: &ObjectName,
3550        ctx: &RenderCtx<'_>,
3551        f: &mut fmt::Formatter<'_>,
3552    ) -> fmt::Result {
3553        match self {
3554            Self::Implicit => name.render(ctx, f),
3555            Self::Keyword(kind) => {
3556                kind.render(ctx, f)?;
3557                f.write_str(" ")?;
3558                name.render(ctx, f)
3559            }
3560            Self::AtAt => {
3561                f.write_str("@@")?;
3562                name.render(ctx, f)
3563            }
3564            Self::AtAtScoped(kind) => {
3565                // The `@@scope.` prefix renders lowercase, matching the canonical
3566                // `Expr::SessionVariable` spelling (`@@global.` / `@@session.`).
3567                f.write_str("@@")?;
3568                f.write_str(kind.at_at_prefix())?;
3569                f.write_str(".")?;
3570                name.render(ctx, f)
3571            }
3572        }
3573    }
3574}
3575
3576impl SystemVariableScopeKind {
3577    /// The lowercase `@@`-form scope prefix word (`global`/`session`/…), matching the
3578    /// canonical `Expr::SessionVariable` spelling.
3579    fn at_at_prefix(self) -> &'static str {
3580        match self {
3581            Self::Global => "global",
3582            Self::Session => "session",
3583            Self::Local => "local",
3584            Self::Persist => "persist",
3585            Self::PersistOnly => "persist_only",
3586        }
3587    }
3588}
3589
3590impl Render for SystemVariableScopeKind {
3591    /// The uppercase keyword-prefix spelling (`SET GLOBAL x = …`).
3592    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3593        f.write_str(match self {
3594            Self::Global => "GLOBAL",
3595            Self::Session => "SESSION",
3596            Self::Local => "LOCAL",
3597            Self::Persist => "PERSIST",
3598            Self::PersistOnly => "PERSIST_ONLY",
3599        })
3600    }
3601}
3602
3603impl<X: Extension + Render> Render for SetVariableValue<X> {
3604    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3605        match self {
3606            Self::Default { .. } => f.write_str("DEFAULT"),
3607            Self::Keyword { keyword, .. } => keyword.render(ctx, f),
3608            Self::Expr { expr, .. } => expr.render(ctx, f),
3609        }
3610    }
3611}
3612
3613impl Render for SetVariableKeyword {
3614    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3615        f.write_str(match self {
3616            Self::On => "ON",
3617            Self::All => "ALL",
3618            Self::Binary => "BINARY",
3619            Self::Row => "ROW",
3620            Self::System => "SYSTEM",
3621        })
3622    }
3623}
3624
3625impl Render for SetCharacterSetValue {
3626    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3627        match self {
3628            Self::Default { .. } => f.write_str("DEFAULT"),
3629            Self::Charset { charset, .. } => charset.render(ctx, f),
3630        }
3631    }
3632}
3633
3634impl Render for AlterSystem {
3635    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3636        f.write_str("ALTER SYSTEM ")?;
3637        match &self.action {
3638            AlterSystemAction::Set {
3639                name,
3640                assignment,
3641                value,
3642                ..
3643            } => {
3644                f.write_str("SET ")?;
3645                name.render(ctx, f)?;
3646                // `=` and `TO` are exact synonyms; a source-fidelity render replays the
3647                // written `=`, a target re-spell and the redacted fingerprint keep `TO`.
3648                f.write_str(match assignment {
3649                    SetAssignment::Equals if honours_source_spelling(ctx) => " = ",
3650                    _ => " TO ",
3651                })?;
3652                value.render(ctx, f)
3653            }
3654            AlterSystemAction::Reset { target, .. } => {
3655                f.write_str("RESET ")?;
3656                target.render(ctx, f)
3657            }
3658        }
3659    }
3660}
3661
3662impl Render for AlterDatabase {
3663    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3664        f.write_str("ALTER DATABASE ")?;
3665        if self.if_exists {
3666            f.write_str("IF EXISTS ")?;
3667        }
3668        self.name.render(ctx, f)?;
3669        match &self.action {
3670            AlterDatabaseAction::SetAlias { new_name, .. } => {
3671                f.write_str(" SET ALIAS TO ")?;
3672                new_name.render(ctx, f)
3673            }
3674        }
3675    }
3676}
3677
3678impl Render for AlterDatabaseOptions {
3679    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3680        f.write_str(match self.spelling {
3681            DatabaseKeyword::Database => "ALTER DATABASE",
3682            DatabaseKeyword::Schema => "ALTER SCHEMA",
3683        })?;
3684        if let Some(name) = &self.name {
3685            f.write_str(" ")?;
3686            name.render(ctx, f)?;
3687        }
3688        for option in &self.options {
3689            f.write_str(" ")?;
3690            option.render(ctx, f)?;
3691        }
3692        Ok(())
3693    }
3694}
3695
3696impl Render for AlterDatabaseOption {
3697    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3698        // `[=]` presence round-trips verbatim (the `IndexLockAlgorithmOption` precedent), so the
3699        // written spacing is reproduced in both source-fidelity and canonical renders.
3700        let sep = |equals: bool| if equals { " = " } else { " " };
3701        match self {
3702            Self::CharacterSet {
3703                default,
3704                keyword,
3705                equals,
3706                charset,
3707                ..
3708            } => {
3709                if *default {
3710                    f.write_str("DEFAULT ")?;
3711                }
3712                f.write_str(match keyword {
3713                    CharsetKeyword::CharacterSet => "CHARACTER SET",
3714                    CharsetKeyword::Charset => "CHARSET",
3715                })?;
3716                f.write_str(sep(*equals))?;
3717                charset.render(ctx, f)
3718            }
3719            Self::Collate {
3720                default,
3721                equals,
3722                collation,
3723                ..
3724            } => {
3725                if *default {
3726                    f.write_str("DEFAULT ")?;
3727                }
3728                f.write_str("COLLATE")?;
3729                f.write_str(sep(*equals))?;
3730                collation.render(ctx, f)
3731            }
3732            Self::Encryption {
3733                default,
3734                equals,
3735                value,
3736                ..
3737            } => {
3738                if *default {
3739                    f.write_str("DEFAULT ")?;
3740                }
3741                f.write_str("ENCRYPTION")?;
3742                f.write_str(sep(*equals))?;
3743                value.render(ctx, f)
3744            }
3745            Self::ReadOnly { equals, value, .. } => {
3746                f.write_str("READ ONLY")?;
3747                f.write_str(sep(*equals))?;
3748                f.write_str(match value {
3749                    ReadOnlyValue::Default => "DEFAULT",
3750                    ReadOnlyValue::Off => "0",
3751                    ReadOnlyValue::On => "1",
3752                })
3753            }
3754        }
3755    }
3756}
3757
3758impl Render for CreateServer {
3759    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3760        f.write_str("CREATE SERVER ")?;
3761        self.name.render(ctx, f)?;
3762        f.write_str(" FOREIGN DATA WRAPPER ")?;
3763        self.wrapper.render(ctx, f)?;
3764        f.write_str(" OPTIONS (")?;
3765        render_server_options(&self.options, ctx, f)?;
3766        f.write_str(")")
3767    }
3768}
3769
3770impl Render for AlterServer {
3771    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3772        f.write_str("ALTER SERVER ")?;
3773        self.name.render(ctx, f)?;
3774        f.write_str(" OPTIONS (")?;
3775        render_server_options(&self.options, ctx, f)?;
3776        f.write_str(")")
3777    }
3778}
3779
3780impl Render for DropServer {
3781    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3782        f.write_str("DROP SERVER ")?;
3783        if self.if_exists {
3784            f.write_str("IF EXISTS ")?;
3785        }
3786        self.name.render(ctx, f)
3787    }
3788}
3789
3790/// The shared `<option>[, ...]` body of a `CREATE`/`ALTER SERVER` `OPTIONS ( … )` list
3791/// (comma-separated, no surrounding parentheses — the caller writes those).
3792fn render_server_options(
3793    options: &[ServerOption],
3794    ctx: &RenderCtx<'_>,
3795    f: &mut fmt::Formatter<'_>,
3796) -> fmt::Result {
3797    for (i, option) in options.iter().enumerate() {
3798        if i > 0 {
3799            f.write_str(", ")?;
3800        }
3801        option.render(ctx, f)?;
3802    }
3803    Ok(())
3804}
3805
3806impl Render for ServerOption {
3807    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3808        f.write_str(match self.kind {
3809            ServerOptionKind::Host => "HOST ",
3810            ServerOptionKind::Database => "DATABASE ",
3811            ServerOptionKind::User => "USER ",
3812            ServerOptionKind::Password => "PASSWORD ",
3813            ServerOptionKind::Socket => "SOCKET ",
3814            ServerOptionKind::Owner => "OWNER ",
3815            ServerOptionKind::Port => "PORT ",
3816        })?;
3817        self.value.render(ctx, f)
3818    }
3819}
3820
3821impl Render for CreateSpatialReferenceSystem {
3822    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3823        f.write_str("CREATE ")?;
3824        if self.or_replace {
3825            f.write_str("OR REPLACE ")?;
3826        }
3827        f.write_str("SPATIAL REFERENCE SYSTEM ")?;
3828        if self.if_not_exists {
3829            f.write_str("IF NOT EXISTS ")?;
3830        }
3831        self.srid.render(ctx, f)?;
3832        for attribute in &self.attributes {
3833            f.write_str(" ")?;
3834            attribute.render(ctx, f)?;
3835        }
3836        Ok(())
3837    }
3838}
3839
3840impl Render for SrsAttribute {
3841    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3842        match self {
3843            Self::Name { value, .. } => {
3844                f.write_str("NAME ")?;
3845                value.render(ctx, f)
3846            }
3847            Self::Definition { value, .. } => {
3848                f.write_str("DEFINITION ")?;
3849                value.render(ctx, f)
3850            }
3851            Self::Organization {
3852                organization,
3853                identifier,
3854                ..
3855            } => {
3856                f.write_str("ORGANIZATION ")?;
3857                organization.render(ctx, f)?;
3858                f.write_str(" IDENTIFIED BY ")?;
3859                identifier.render(ctx, f)
3860            }
3861            Self::Description { value, .. } => {
3862                f.write_str("DESCRIPTION ")?;
3863                value.render(ctx, f)
3864            }
3865        }
3866    }
3867}
3868
3869impl Render for DropSpatialReferenceSystem {
3870    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3871        f.write_str("DROP SPATIAL REFERENCE SYSTEM ")?;
3872        if self.if_exists {
3873            f.write_str("IF EXISTS ")?;
3874        }
3875        self.srid.render(ctx, f)
3876    }
3877}
3878
3879impl Render for CreateResourceGroup {
3880    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3881        f.write_str("CREATE RESOURCE GROUP ")?;
3882        self.name.render(ctx, f)?;
3883        f.write_str(if self.type_equals {
3884            " TYPE = "
3885        } else {
3886            " TYPE "
3887        })?;
3888        self.group_type.render(ctx, f)?;
3889        if let Some(vcpu) = &self.vcpu {
3890            f.write_str(" ")?;
3891            vcpu.render(ctx, f)?;
3892        }
3893        if let Some(priority) = &self.thread_priority {
3894            f.write_str(" ")?;
3895            priority.render(ctx, f)?;
3896        }
3897        if let Some(state) = &self.state {
3898            f.write_str(" ")?;
3899            state.render(ctx, f)?;
3900        }
3901        Ok(())
3902    }
3903}
3904
3905impl Render for AlterResourceGroup {
3906    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3907        f.write_str("ALTER RESOURCE GROUP ")?;
3908        self.name.render(ctx, f)?;
3909        if let Some(vcpu) = &self.vcpu {
3910            f.write_str(" ")?;
3911            vcpu.render(ctx, f)?;
3912        }
3913        if let Some(priority) = &self.thread_priority {
3914            f.write_str(" ")?;
3915            priority.render(ctx, f)?;
3916        }
3917        if let Some(state) = &self.state {
3918            f.write_str(" ")?;
3919            state.render(ctx, f)?;
3920        }
3921        if self.force {
3922            f.write_str(" FORCE")?;
3923        }
3924        Ok(())
3925    }
3926}
3927
3928impl Render for DropResourceGroup {
3929    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3930        f.write_str("DROP RESOURCE GROUP ")?;
3931        self.name.render(ctx, f)?;
3932        if self.force {
3933            f.write_str(" FORCE")?;
3934        }
3935        Ok(())
3936    }
3937}
3938
3939impl Render for ResourceGroupType {
3940    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3941        f.write_str(match self {
3942            Self::System => "SYSTEM",
3943            Self::User => "USER",
3944        })
3945    }
3946}
3947
3948impl Render for ResourceGroupState {
3949    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3950        f.write_str(match self {
3951            Self::Enable => "ENABLE",
3952            Self::Disable => "DISABLE",
3953        })
3954    }
3955}
3956
3957impl Render for ResourceGroupVcpu {
3958    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3959        f.write_str(if self.equals { "VCPU = " } else { "VCPU " })?;
3960        for (i, range) in self.ranges.iter().enumerate() {
3961            if i > 0 {
3962                f.write_str(", ")?;
3963            }
3964            range.render(ctx, f)?;
3965        }
3966        Ok(())
3967    }
3968}
3969
3970impl Render for VcpuRange {
3971    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3972        self.start.render(ctx, f)?;
3973        if let Some(end) = &self.end {
3974            f.write_str("-")?;
3975            end.render(ctx, f)?;
3976        }
3977        Ok(())
3978    }
3979}
3980
3981impl Render for ResourceGroupThreadPriority {
3982    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3983        f.write_str(if self.equals {
3984            "THREAD_PRIORITY = "
3985        } else {
3986            "THREAD_PRIORITY "
3987        })?;
3988        if self.negative {
3989            f.write_str("-")?;
3990        }
3991        self.value.render(ctx, f)
3992    }
3993}
3994
3995impl Render for AlterInstance {
3996    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3997        f.write_str("ALTER INSTANCE ")?;
3998        self.action.render(ctx, f)
3999    }
4000}
4001
4002impl Render for AlterInstanceAction {
4003    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4004        match self {
4005            Self::RotateInnodbMasterKey { .. } => f.write_str("ROTATE INNODB MASTER KEY"),
4006            Self::RotateBinlogMasterKey { .. } => f.write_str("ROTATE BINLOG MASTER KEY"),
4007            Self::ReloadTls {
4008                channel,
4009                no_rollback_on_error,
4010                ..
4011            } => {
4012                f.write_str("RELOAD TLS")?;
4013                if let Some(channel) = channel {
4014                    f.write_str(" FOR CHANNEL ")?;
4015                    channel.render(ctx, f)?;
4016                }
4017                if *no_rollback_on_error {
4018                    f.write_str(" NO ROLLBACK ON ERROR")?;
4019                }
4020                Ok(())
4021            }
4022            Self::ReloadKeyring { .. } => f.write_str("RELOAD KEYRING"),
4023            Self::EnableInnodbRedoLog { .. } => f.write_str("ENABLE INNODB REDO_LOG"),
4024            Self::DisableInnodbRedoLog { .. } => f.write_str("DISABLE INNODB REDO_LOG"),
4025        }
4026    }
4027}
4028
4029impl<X: Extension + Render> Render for AlterSequence<X> {
4030    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4031        f.write_str("ALTER SEQUENCE ")?;
4032        if self.if_exists {
4033            f.write_str("IF EXISTS ")?;
4034        }
4035        self.name.render(ctx, f)?;
4036        // Options are space-separated in their canonical spelling; the shared core reuses the
4037        // `IdentityOption` render.
4038        for option in &self.options {
4039            f.write_str(" ")?;
4040            option.render(ctx, f)?;
4041        }
4042        Ok(())
4043    }
4044}
4045
4046impl<X: Extension + Render> Render for AlterSequenceOption<X> {
4047    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4048        match self {
4049            Self::Common { option, .. } => option.render(ctx, f),
4050            Self::Restart { value: None, .. } => f.write_str("RESTART"),
4051            Self::Restart {
4052                value: Some(expr), ..
4053            } => {
4054                f.write_str("RESTART WITH ")?;
4055                expr.render(ctx, f)
4056            }
4057            Self::As { data_type, .. } => {
4058                f.write_str("AS ")?;
4059                data_type.render(ctx, f)
4060            }
4061            Self::OwnedBy { owner: None, .. } => f.write_str("OWNED BY NONE"),
4062            Self::OwnedBy {
4063                owner: Some(name), ..
4064            } => {
4065                f.write_str("OWNED BY ")?;
4066                name.render(ctx, f)
4067            }
4068            Self::SequenceName { name, .. } => {
4069                f.write_str("SEQUENCE NAME ")?;
4070                name.render(ctx, f)
4071            }
4072        }
4073    }
4074}
4075
4076impl Render for AlterObjectSchema {
4077    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4078        f.write_str("ALTER ")?;
4079        f.write_str(match self.object_type {
4080            SchemaRelocationObject::Table => "TABLE ",
4081            SchemaRelocationObject::View => "VIEW ",
4082            SchemaRelocationObject::Sequence => "SEQUENCE ",
4083        })?;
4084        if self.if_exists {
4085            f.write_str("IF EXISTS ")?;
4086        }
4087        self.name.render(ctx, f)?;
4088        f.write_str(" SET SCHEMA ")?;
4089        self.new_schema.render(ctx, f)
4090    }
4091}
4092
4093/// Render an optional `SET` scope as ` SESSION` / ` LOCAL` (leading space), or
4094/// nothing when absent — shared by the generic and special `SET` forms.
4095fn render_set_scope(
4096    scope: Option<SetScope>,
4097    ctx: &RenderCtx<'_>,
4098    f: &mut fmt::Formatter<'_>,
4099) -> fmt::Result {
4100    if let Some(scope) = scope {
4101        f.write_str(" ")?;
4102        scope.render(ctx, f)?;
4103    }
4104    Ok(())
4105}
4106
4107impl Render for SpecialSetValue {
4108    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4109        match self {
4110            Self::Default { .. } => f.write_str("DEFAULT"),
4111            Self::Local { .. } => f.write_str("LOCAL"),
4112            Self::None { .. } => f.write_str("NONE"),
4113            Self::Value { value, .. } => value.render(ctx, f),
4114        }
4115    }
4116}
4117
4118impl Render for ConstraintsTarget {
4119    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4120        match self {
4121            Self::All { .. } => f.write_str("ALL"),
4122            Self::Names { names, .. } => render_comma_separated(names, ctx, f),
4123        }
4124    }
4125}
4126
4127impl Render for ConstraintCheckTime {
4128    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4129        f.write_str(match self {
4130            Self::Deferred => "DEFERRED",
4131            Self::Immediate => "IMMEDIATE",
4132        })
4133    }
4134}
4135
4136impl Render for SetNamesValue {
4137    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4138        match self {
4139            Self::Default { .. } => f.write_str("DEFAULT"),
4140            Self::Charset {
4141                charset, collation, ..
4142            } => {
4143                charset.render(ctx, f)?;
4144                if let Some(collation) = collation {
4145                    f.write_str(" COLLATE ")?;
4146                    collation.render(ctx, f)?;
4147                }
4148                Ok(())
4149            }
4150        }
4151    }
4152}
4153
4154impl Render for SetScope {
4155    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4156        f.write_str(match self {
4157            Self::Session => "SESSION",
4158            Self::Local => "LOCAL",
4159            Self::Global => "GLOBAL",
4160        })
4161    }
4162}
4163
4164impl Render for SetValue {
4165    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4166        match self {
4167            Self::Default { .. } => f.write_str("DEFAULT"),
4168            Self::Values { values, .. } => render_comma_separated(values, ctx, f),
4169        }
4170    }
4171}
4172
4173impl Render for SetParameterValue {
4174    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4175        match self {
4176            Self::Literal { literal, .. } => literal.render(ctx, f),
4177            Self::Name { name, .. } => name.render(ctx, f),
4178            Self::List { values, .. } => {
4179                f.write_str("[")?;
4180                render_comma_separated(values, ctx, f)?;
4181                f.write_str("]")
4182            }
4183        }
4184    }
4185}
4186
4187impl Render for ConfigParameter {
4188    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4189        match self {
4190            Self::All { .. } => f.write_str("ALL"),
4191            Self::Named { name, .. } => name.render(ctx, f),
4192        }
4193    }
4194}
4195
4196impl<X: Extension + Render> Render for AccessControlStatement<X> {
4197    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4198        match self {
4199            Self::Grant {
4200                privileges,
4201                object,
4202                grantees,
4203                with_grant_option,
4204                granted_by,
4205                ..
4206            } => {
4207                f.write_str("GRANT ")?;
4208                privileges.render(ctx, f)?;
4209                f.write_str(" ON ")?;
4210                object.render(ctx, f)?;
4211                f.write_str(" TO ")?;
4212                render_comma_separated(grantees, ctx, f)?;
4213                if *with_grant_option {
4214                    f.write_str(" WITH GRANT OPTION")?;
4215                }
4216                render_granted_by(granted_by, ctx, f)
4217            }
4218            Self::Revoke {
4219                grant_option_for,
4220                privileges,
4221                object,
4222                grantees,
4223                granted_by,
4224                behavior,
4225                ..
4226            } => {
4227                f.write_str("REVOKE ")?;
4228                if *grant_option_for {
4229                    f.write_str("GRANT OPTION FOR ")?;
4230                }
4231                privileges.render(ctx, f)?;
4232                f.write_str(" ON ")?;
4233                object.render(ctx, f)?;
4234                f.write_str(" FROM ")?;
4235                render_comma_separated(grantees, ctx, f)?;
4236                render_granted_by(granted_by, ctx, f)?;
4237                render_drop_behavior(*behavior, ctx, f)
4238            }
4239            Self::GrantRole {
4240                roles,
4241                grantees,
4242                with_admin_option,
4243                granted_by,
4244                ..
4245            } => {
4246                f.write_str("GRANT ")?;
4247                render_ident_list(roles, ctx, f)?;
4248                f.write_str(" TO ")?;
4249                render_comma_separated(grantees, ctx, f)?;
4250                if *with_admin_option {
4251                    f.write_str(" WITH ADMIN OPTION")?;
4252                }
4253                render_granted_by(granted_by, ctx, f)
4254            }
4255            Self::RevokeRole {
4256                admin_option_for,
4257                roles,
4258                grantees,
4259                granted_by,
4260                behavior,
4261                ..
4262            } => {
4263                f.write_str("REVOKE ")?;
4264                if *admin_option_for {
4265                    f.write_str("ADMIN OPTION FOR ")?;
4266                }
4267                render_ident_list(roles, ctx, f)?;
4268                f.write_str(" FROM ")?;
4269                render_comma_separated(grantees, ctx, f)?;
4270                render_granted_by(granted_by, ctx, f)?;
4271                render_drop_behavior(*behavior, ctx, f)
4272            }
4273            Self::AccountGrantPrivilege {
4274                privileges,
4275                object,
4276                grantees,
4277                with_grant_option,
4278                grant_as,
4279                ..
4280            } => {
4281                f.write_str("GRANT ")?;
4282                privileges.render(ctx, f)?;
4283                f.write_str(" ON ")?;
4284                object.render(ctx, f)?;
4285                f.write_str(" TO ")?;
4286                render_comma_separated(grantees, ctx, f)?;
4287                if *with_grant_option {
4288                    f.write_str(" WITH GRANT OPTION")?;
4289                }
4290                if let Some(grant_as) = grant_as {
4291                    f.write_str(" ")?;
4292                    grant_as.render(ctx, f)?;
4293                }
4294                Ok(())
4295            }
4296            Self::AccountGrantProxy {
4297                proxy,
4298                grantees,
4299                with_grant_option,
4300                ..
4301            } => {
4302                f.write_str("GRANT PROXY ON ")?;
4303                proxy.render(ctx, f)?;
4304                f.write_str(" TO ")?;
4305                render_comma_separated(grantees, ctx, f)?;
4306                if *with_grant_option {
4307                    f.write_str(" WITH GRANT OPTION")?;
4308                }
4309                Ok(())
4310            }
4311            Self::AccountGrantRole {
4312                roles,
4313                grantees,
4314                with_admin_option,
4315                ..
4316            } => {
4317                f.write_str("GRANT ")?;
4318                render_comma_separated(roles, ctx, f)?;
4319                f.write_str(" TO ")?;
4320                render_comma_separated(grantees, ctx, f)?;
4321                if *with_admin_option {
4322                    f.write_str(" WITH ADMIN OPTION")?;
4323                }
4324                Ok(())
4325            }
4326            Self::AccountRevokePrivilege {
4327                if_exists,
4328                privileges,
4329                object,
4330                grantees,
4331                ignore_unknown_user,
4332                ..
4333            } => {
4334                f.write_str("REVOKE ")?;
4335                if *if_exists {
4336                    f.write_str("IF EXISTS ")?;
4337                }
4338                privileges.render(ctx, f)?;
4339                f.write_str(" ON ")?;
4340                object.render(ctx, f)?;
4341                f.write_str(" FROM ")?;
4342                render_comma_separated(grantees, ctx, f)?;
4343                render_ignore_unknown_user(*ignore_unknown_user, f)
4344            }
4345            Self::AccountRevokeAll {
4346                if_exists,
4347                privileges_keyword,
4348                grantees,
4349                ignore_unknown_user,
4350                ..
4351            } => {
4352                f.write_str("REVOKE ")?;
4353                if *if_exists {
4354                    f.write_str("IF EXISTS ")?;
4355                }
4356                // The optional `PRIVILEGES` noise word is fidelity-only; the canonical render
4357                // emits it.
4358                if *privileges_keyword || !honours_source_spelling(ctx) {
4359                    f.write_str("ALL PRIVILEGES, GRANT OPTION FROM ")?;
4360                } else {
4361                    f.write_str("ALL, GRANT OPTION FROM ")?;
4362                }
4363                render_comma_separated(grantees, ctx, f)?;
4364                render_ignore_unknown_user(*ignore_unknown_user, f)
4365            }
4366            Self::AccountRevokeProxy {
4367                if_exists,
4368                proxy,
4369                grantees,
4370                ignore_unknown_user,
4371                ..
4372            } => {
4373                f.write_str("REVOKE ")?;
4374                if *if_exists {
4375                    f.write_str("IF EXISTS ")?;
4376                }
4377                f.write_str("PROXY ON ")?;
4378                proxy.render(ctx, f)?;
4379                f.write_str(" FROM ")?;
4380                render_comma_separated(grantees, ctx, f)?;
4381                render_ignore_unknown_user(*ignore_unknown_user, f)
4382            }
4383            Self::AccountRevokeRole {
4384                if_exists,
4385                roles,
4386                grantees,
4387                ignore_unknown_user,
4388                ..
4389            } => {
4390                f.write_str("REVOKE ")?;
4391                if *if_exists {
4392                    f.write_str("IF EXISTS ")?;
4393                }
4394                render_comma_separated(roles, ctx, f)?;
4395                f.write_str(" FROM ")?;
4396                render_comma_separated(grantees, ctx, f)?;
4397                render_ignore_unknown_user(*ignore_unknown_user, f)
4398            }
4399        }
4400    }
4401}
4402
4403/// Render the MySQL `IGNORE UNKNOWN USER` trailer, when present.
4404fn render_ignore_unknown_user(
4405    ignore_unknown_user: bool,
4406    f: &mut fmt::Formatter<'_>,
4407) -> fmt::Result {
4408    if ignore_unknown_user {
4409        f.write_str(" IGNORE UNKNOWN USER")?;
4410    }
4411    Ok(())
4412}
4413
4414impl Render for PrivilegeLevelObject {
4415    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4416        match self.object_type {
4417            PrivilegeObjectType::Table { explicit } => {
4418                if explicit {
4419                    f.write_str("TABLE ")?;
4420                }
4421            }
4422            PrivilegeObjectType::Function => f.write_str("FUNCTION ")?,
4423            PrivilegeObjectType::Procedure => f.write_str("PROCEDURE ")?,
4424        }
4425        self.level.render(ctx, f)
4426    }
4427}
4428
4429impl Render for PrivilegeLevel {
4430    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4431        match self {
4432            Self::Global { .. } => f.write_str("*.*"),
4433            Self::CurrentDatabase { .. } => f.write_str("*"),
4434            Self::Database { database, .. } => {
4435                database.render(ctx, f)?;
4436                f.write_str(".*")
4437            }
4438            Self::Object { name, .. } => name.render(ctx, f),
4439        }
4440    }
4441}
4442
4443impl Render for GrantAs {
4444    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4445        f.write_str("AS ")?;
4446        self.user.render(ctx, f)?;
4447        if let Some(with_role) = &self.with_role {
4448            f.write_str(" WITH ROLE ")?;
4449            with_role.render(ctx, f)?;
4450        }
4451        Ok(())
4452    }
4453}
4454
4455impl Render for WithRoleSpec {
4456    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4457        match self {
4458            Self::Roles { roles, .. } => render_comma_separated(roles, ctx, f),
4459            Self::All { except, .. } => {
4460                f.write_str("ALL")?;
4461                if !except.is_empty() {
4462                    f.write_str(" EXCEPT ")?;
4463                    render_comma_separated(except, ctx, f)?;
4464                }
4465                Ok(())
4466            }
4467            Self::None { .. } => f.write_str("NONE"),
4468            Self::Default { .. } => f.write_str("DEFAULT"),
4469        }
4470    }
4471}
4472
4473/// Render an optional `GRANTED BY <grantor>` trailer.
4474fn render_granted_by(
4475    granted_by: &Option<RoleSpec>,
4476    ctx: &RenderCtx<'_>,
4477    f: &mut fmt::Formatter<'_>,
4478) -> fmt::Result {
4479    if let Some(grantor) = granted_by {
4480        f.write_str(" GRANTED BY ")?;
4481        grantor.render(ctx, f)?;
4482    }
4483    Ok(())
4484}
4485
4486impl Render for Privileges {
4487    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4488        match self {
4489            // The optional `PRIVILEGES` keyword is exact-synonym sugar: the canonical
4490            // render emits it, a source-fidelity render drops it when the source did.
4491            Self::All {
4492                privileges_keyword, ..
4493            } => {
4494                if *privileges_keyword || !honours_source_spelling(ctx) {
4495                    f.write_str("ALL PRIVILEGES")
4496                } else {
4497                    f.write_str("ALL")
4498                }
4499            }
4500            Self::List { privileges, .. } => render_comma_separated(privileges, ctx, f),
4501        }
4502    }
4503}
4504
4505impl Render for Privilege {
4506    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4507        match self {
4508            Self::Known { kind, columns, .. } => {
4509                kind.render(ctx, f)?;
4510                render_privilege_columns(columns, ctx, f)
4511            }
4512            Self::Other { name, columns, .. } => {
4513                name.render(ctx, f)?;
4514                render_privilege_columns(columns, ctx, f)
4515            }
4516        }
4517    }
4518}
4519
4520/// Render a privilege's optional `( <column> [, ...] )` scope.
4521fn render_privilege_columns(
4522    columns: &[Ident],
4523    ctx: &RenderCtx<'_>,
4524    f: &mut fmt::Formatter<'_>,
4525) -> fmt::Result {
4526    if !columns.is_empty() {
4527        f.write_str(" (")?;
4528        render_ident_list(columns, ctx, f)?;
4529        f.write_str(")")?;
4530    }
4531    Ok(())
4532}
4533
4534impl Render for PrivilegeKind {
4535    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4536        f.write_str(match self {
4537            Self::Select => "SELECT",
4538            Self::Insert => "INSERT",
4539            Self::Update => "UPDATE",
4540            Self::Delete => "DELETE",
4541            Self::Truncate => "TRUNCATE",
4542            Self::References => "REFERENCES",
4543            Self::Trigger => "TRIGGER",
4544            Self::Usage => "USAGE",
4545            Self::Execute => "EXECUTE",
4546            Self::Create => "CREATE",
4547            Self::Connect => "CONNECT",
4548            Self::Temporary => "TEMPORARY",
4549            Self::Temp => "TEMP",
4550            Self::Maintain => "MAINTAIN",
4551            Self::Index => "INDEX",
4552            Self::Alter => "ALTER",
4553            Self::Drop => "DROP",
4554            Self::Reload => "RELOAD",
4555            Self::Shutdown => "SHUTDOWN",
4556            Self::Process => "PROCESS",
4557            Self::File => "FILE",
4558            Self::Super => "SUPER",
4559            Self::Event => "EVENT",
4560            Self::GrantOption => "GRANT OPTION",
4561            Self::ShowDatabases => "SHOW DATABASES",
4562            Self::CreateTemporaryTables => "CREATE TEMPORARY TABLES",
4563            Self::LockTables => "LOCK TABLES",
4564            Self::ReplicationSlave => "REPLICATION SLAVE",
4565            Self::ReplicationClient => "REPLICATION CLIENT",
4566            Self::CreateView => "CREATE VIEW",
4567            Self::ShowView => "SHOW VIEW",
4568            Self::CreateRoutine => "CREATE ROUTINE",
4569            Self::AlterRoutine => "ALTER ROUTINE",
4570            Self::CreateUser => "CREATE USER",
4571            Self::CreateTablespace => "CREATE TABLESPACE",
4572            Self::CreateRole => "CREATE ROLE",
4573            Self::DropRole => "DROP ROLE",
4574        })
4575    }
4576}
4577
4578impl<X: Extension + Render> Render for GrantObject<X> {
4579    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4580        match self {
4581            Self::Table {
4582                explicit, names, ..
4583            } => {
4584                if *explicit {
4585                    f.write_str("TABLE ")?;
4586                }
4587                render_comma_separated(names, ctx, f)
4588            }
4589            Self::Named { kind, names, .. } => {
4590                kind.render(ctx, f)?;
4591                f.write_str(" ")?;
4592                render_comma_separated(names, ctx, f)
4593            }
4594            Self::Routines { kind, routines, .. } => {
4595                kind.render(ctx, f)?;
4596                f.write_str(" ")?;
4597                render_comma_separated(routines, ctx, f)
4598            }
4599            Self::AllInSchema { kind, schemas, .. } => {
4600                f.write_str("ALL ")?;
4601                kind.render(ctx, f)?;
4602                f.write_str(" IN SCHEMA ")?;
4603                render_comma_separated(schemas, ctx, f)
4604            }
4605        }
4606    }
4607}
4608
4609impl Render for NamedObjectKind {
4610    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4611        f.write_str(match self {
4612            Self::Sequence => "SEQUENCE",
4613            Self::Database => "DATABASE",
4614            Self::Schema => "SCHEMA",
4615            Self::Domain => "DOMAIN",
4616            Self::Type => "TYPE",
4617            Self::Language => "LANGUAGE",
4618            Self::Tablespace => "TABLESPACE",
4619            Self::ForeignDataWrapper => "FOREIGN DATA WRAPPER",
4620            Self::ForeignServer => "FOREIGN SERVER",
4621        })
4622    }
4623}
4624
4625impl Render for RoutineObjectKind {
4626    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4627        f.write_str(match self {
4628            Self::Function => "FUNCTION",
4629            Self::Procedure => "PROCEDURE",
4630            Self::Routine => "ROUTINE",
4631        })
4632    }
4633}
4634
4635impl Render for SchemaObjectKind {
4636    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4637        f.write_str(match self {
4638            Self::Tables => "TABLES",
4639            Self::Sequences => "SEQUENCES",
4640            Self::Functions => "FUNCTIONS",
4641            Self::Procedures => "PROCEDURES",
4642            Self::Routines => "ROUTINES",
4643        })
4644    }
4645}
4646
4647impl<X: Extension + Render> Render for RoutineSignature<X> {
4648    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4649        self.name.render(ctx, f)?;
4650        if let Some(arg_types) = &self.arg_types {
4651            f.write_str("(")?;
4652            render_comma_separated(arg_types, ctx, f)?;
4653            f.write_str(")")?;
4654        }
4655        Ok(())
4656    }
4657}
4658
4659impl Render for Grantee {
4660    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4661        if self.group {
4662            f.write_str("GROUP ")?;
4663        }
4664        self.spec.render(ctx, f)
4665    }
4666}
4667
4668impl Render for RoleSpec {
4669    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4670        match self {
4671            Self::Public { .. } => f.write_str("PUBLIC"),
4672            Self::CurrentRole { .. } => f.write_str("CURRENT_ROLE"),
4673            Self::CurrentUser { .. } => f.write_str("CURRENT_USER"),
4674            Self::SessionUser { .. } => f.write_str("SESSION_USER"),
4675            Self::Name { name, .. } => name.render(ctx, f),
4676        }
4677    }
4678}
4679
4680impl<X: Extension + Render> Render for CopyStatement<X> {
4681    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4682        f.write_str("COPY ")?;
4683        // The `opt_binary` prefix, when written.
4684        if self.binary {
4685            f.write_str("BINARY ")?;
4686        }
4687        self.source.render(ctx, f)?;
4688        f.write_str(" ")?;
4689        self.direction.render(ctx, f)?;
4690        f.write_str(" ")?;
4691        self.target.render(ctx, f)?;
4692        // The `[USING] DELIMITERS '<str>'` clause, rendered in canonical form
4693        // (the optional `USING` is dropped) between the endpoint and the options.
4694        if let Some(delimiters) = &self.delimiters {
4695            f.write_str(" DELIMITERS ")?;
4696            delimiters.render(ctx, f)?;
4697        }
4698        // An empty option list renders nothing in either spelling, so the surface
4699        // tag only matters when populated. The parenthesized spelling canonicalizes
4700        // the optional `WITH` as present; the legacy spelling is space-separated
4701        // with neither `WITH` nor parentheses.
4702        if !self.options.is_empty() {
4703            if self.parenthesized {
4704                f.write_str(" WITH (")?;
4705                render_comma_separated(&self.options, ctx, f)?;
4706                f.write_str(")")?;
4707            } else {
4708                for option in &self.options {
4709                    f.write_str(" ")?;
4710                    option.render(ctx, f)?;
4711                }
4712            }
4713        }
4714        // The `COPY FROM ... WHERE <predicate>` filter, last (the parser only ever
4715        // populates it on a `FROM` table source).
4716        if let Some(filter) = &self.filter {
4717            f.write_str(" WHERE ")?;
4718            filter.render(ctx, f)?;
4719        }
4720        Ok(())
4721    }
4722}
4723
4724impl<X: Extension + Render> Render for CopySource<X> {
4725    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4726        match self {
4727            Self::Table { table, columns, .. } => {
4728                table.render(ctx, f)?;
4729                if !columns.is_empty() {
4730                    f.write_str(" (")?;
4731                    render_ident_list(columns, ctx, f)?;
4732                    f.write_str(")")?;
4733                }
4734                Ok(())
4735            }
4736            Self::Query { query, .. } => {
4737                f.write_str("(")?;
4738                query.render(ctx, f)?;
4739                f.write_str(")")
4740            }
4741        }
4742    }
4743}
4744
4745impl Render for CopyDirection {
4746    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4747        f.write_str(match self {
4748            Self::From => "FROM",
4749            Self::To => "TO",
4750        })
4751    }
4752}
4753
4754impl Render for CopyTarget {
4755    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4756        match self {
4757            Self::File { path, .. } => path.render(ctx, f),
4758            Self::Stdin { .. } => f.write_str("STDIN"),
4759            Self::Stdout { .. } => f.write_str("STDOUT"),
4760            Self::Program { command, .. } => {
4761                f.write_str("PROGRAM ")?;
4762                command.render(ctx, f)
4763            }
4764        }
4765    }
4766}
4767
4768impl Render for CopyOption {
4769    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4770        self.name.render(ctx, f)?;
4771        if let Some(value) = &self.value {
4772            f.write_str(" ")?;
4773            value.render(ctx, f)?;
4774        }
4775        Ok(())
4776    }
4777}
4778
4779impl Render for CopyOptionValue {
4780    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4781        match self {
4782            Self::Word { word, .. } => word.render(ctx, f),
4783            Self::String { value, .. } => value.render(ctx, f),
4784            Self::Number { value, .. } => value.render(ctx, f),
4785            Self::Star { .. } => f.write_str("*"),
4786            Self::List { values, .. } => {
4787                f.write_str("(")?;
4788                render_comma_separated(values, ctx, f)?;
4789                f.write_str(")")
4790            }
4791            // The Snowflake `FILE_FORMAT = (TYPE = CSV ...)` nested list: space-separated
4792            // `key = value` pairs (the `= (...)` argument spelling), never PostgreSQL's
4793            // comma-separated `List` shape above.
4794            Self::OptionList { options, .. } => {
4795                f.write_str("(")?;
4796                render_copy_into_options(options, ctx, f)?;
4797                f.write_str(")")
4798            }
4799            Self::Force { kind, columns, .. } => {
4800                kind.render(ctx, f)?;
4801                f.write_str(" ")?;
4802                // An empty column list is the `*` (all-columns) form.
4803                if columns.is_empty() {
4804                    f.write_str("*")
4805                } else {
4806                    render_ident_list(columns, ctx, f)
4807                }
4808            }
4809        }
4810    }
4811}
4812
4813impl Render for ForceKind {
4814    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4815        f.write_str(match self {
4816            Self::Quote => "QUOTE",
4817            Self::Null => "NULL",
4818            Self::NotNull => "NOT NULL",
4819        })
4820    }
4821}
4822
4823/// Render the Snowflake `COPY INTO` option list: space-separated `<name> = <value>`
4824/// pairs (the `KEY = VALUE` spelling), reused for both the top-level option list and
4825/// the nested [`CopyOptionValue::OptionList`] argument. A valueless option — which
4826/// Snowflake's grammar never emits, but the shared [`CopyOption`] type permits —
4827/// renders as the bare name.
4828fn render_copy_into_options(
4829    options: &[CopyOption],
4830    ctx: &RenderCtx<'_>,
4831    f: &mut fmt::Formatter<'_>,
4832) -> fmt::Result {
4833    for (i, option) in options.iter().enumerate() {
4834        if i > 0 {
4835            f.write_str(" ")?;
4836        }
4837        option.name.render(ctx, f)?;
4838        if let Some(value) = &option.value {
4839            f.write_str(" = ")?;
4840            value.render(ctx, f)?;
4841        }
4842    }
4843    Ok(())
4844}
4845
4846impl<X: Extension + Render> Render for CopyIntoStatement<X> {
4847    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4848        f.write_str("COPY INTO ")?;
4849        self.target.render(ctx, f)?;
4850        f.write_str(" FROM ")?;
4851        self.source.render(ctx, f)?;
4852        if !self.options.is_empty() {
4853            f.write_str(" ")?;
4854            render_copy_into_options(&self.options, ctx, f)?;
4855        }
4856        Ok(())
4857    }
4858}
4859
4860impl Render for ExportStatement {
4861    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4862        f.write_str("EXPORT DATABASE ")?;
4863        // A named catalogue reconstructs the required `<db> TO` before the path; the bare
4864        // form renders just the path.
4865        if let Some(database) = &self.database {
4866            database.render(ctx, f)?;
4867            f.write_str(" TO ")?;
4868        }
4869        self.path.render(ctx, f)?;
4870        // The option trailer mirrors `COPY`'s two spellings, minus the leading `WITH` the
4871        // `EXPORT` grammar omits: the parenthesized form renders bare `(...)`, the legacy
4872        // form space-separated. An empty list renders nothing in either spelling.
4873        if !self.options.is_empty() {
4874            if self.parenthesized {
4875                f.write_str(" (")?;
4876                render_comma_separated(&self.options, ctx, f)?;
4877                f.write_str(")")?;
4878            } else {
4879                for option in &self.options {
4880                    f.write_str(" ")?;
4881                    option.render(ctx, f)?;
4882                }
4883            }
4884        }
4885        Ok(())
4886    }
4887}
4888
4889impl Render for ImportStatement {
4890    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4891        f.write_str("IMPORT DATABASE ")?;
4892        self.path.render(ctx, f)
4893    }
4894}
4895
4896impl Render for CopyIntoTarget {
4897    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4898        match self {
4899            Self::Table { table, columns, .. } => {
4900                table.render(ctx, f)?;
4901                if !columns.is_empty() {
4902                    f.write_str(" (")?;
4903                    render_ident_list(columns, ctx, f)?;
4904                    f.write_str(")")?;
4905                }
4906                Ok(())
4907            }
4908            Self::External { location, .. } => location.render(ctx, f),
4909            Self::Stage { reference, .. } => reference.render(ctx, f),
4910        }
4911    }
4912}
4913
4914impl<X: Extension + Render> Render for CopyIntoSource<X> {
4915    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4916        match self {
4917            Self::Table { table, .. } => table.render(ctx, f),
4918            Self::External { location, .. } => location.render(ctx, f),
4919            Self::Stage { reference, .. } => reference.render(ctx, f),
4920            Self::Query { query, .. } => {
4921                f.write_str("(")?;
4922                query.render(ctx, f)?;
4923                f.write_str(")")
4924            }
4925        }
4926    }
4927}
4928
4929impl Render for ExplainKeyword {
4930    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4931        f.write_str(match self {
4932            Self::Explain => "EXPLAIN",
4933            Self::Describe => "DESCRIBE",
4934            Self::Desc => "DESC",
4935        })
4936    }
4937}
4938
4939impl<X: Extension + Render> Render for ExplainStatement<X> {
4940    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4941        self.spelling.render(ctx, f)?;
4942        if self.parenthesized {
4943            f.write_str(" (")?;
4944            render_comma_separated(&self.options, ctx, f)?;
4945            f.write_str(")")?;
4946        } else {
4947            // The legacy keyword prefix: each option is a bare space-separated word.
4948            for option in &self.options {
4949                f.write_str(" ")?;
4950                option.render(ctx, f)?;
4951            }
4952        }
4953        f.write_str(" ")?;
4954        self.statement.render(ctx, f)
4955    }
4956}
4957
4958impl Render for ExplainOption {
4959    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4960        match self {
4961            Self::Analyze { value, .. } => {
4962                render_explain_word_option("ANALYZE", value.as_ref(), ctx, f)
4963            }
4964            Self::Verbose { value, .. } => {
4965                render_explain_word_option("VERBOSE", value.as_ref(), ctx, f)
4966            }
4967            Self::Format { format, .. } => {
4968                f.write_str("FORMAT ")?;
4969                format.render(ctx, f)
4970            }
4971            Self::Other { name, value, .. } => {
4972                name.render(ctx, f)?;
4973                if let Some(value) = value {
4974                    f.write_str(" ")?;
4975                    value.render(ctx, f)?;
4976                }
4977                Ok(())
4978            }
4979        }
4980    }
4981}
4982
4983/// Render a built-in `EXPLAIN` option keyword with its optional argument word.
4984fn render_explain_word_option(
4985    keyword: &str,
4986    value: Option<&Ident>,
4987    ctx: &RenderCtx<'_>,
4988    f: &mut fmt::Formatter<'_>,
4989) -> fmt::Result {
4990    f.write_str(keyword)?;
4991    if let Some(value) = value {
4992        f.write_str(" ")?;
4993        value.render(ctx, f)?;
4994    }
4995    Ok(())
4996}
4997
4998impl Render for ExplainFormat {
4999    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5000        f.write_str(match self {
5001            Self::Text => "TEXT",
5002            Self::Xml => "XML",
5003            Self::Json => "JSON",
5004            Self::Yaml => "YAML",
5005        })
5006    }
5007}
5008
5009impl Render for DescribeStatement {
5010    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5011        self.keyword.render(ctx, f)?;
5012        f.write_str(" ")?;
5013        self.table.render(ctx, f)?;
5014        if let Some(column) = &self.column {
5015            f.write_str(" ")?;
5016            column.render(ctx, f)?;
5017        }
5018        Ok(())
5019    }
5020}
5021
5022impl Render for DescribeColumn {
5023    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5024        match self {
5025            Self::Name { name, .. } => name.render(ctx, f),
5026            Self::Wild { pattern, .. } => pattern.render(ctx, f),
5027        }
5028    }
5029}
5030
5031impl<X: Extension + Render> Render for ShowStatement<X> {
5032    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5033        self.target.render(ctx, f)
5034    }
5035}
5036
5037impl<X: Extension + Render> Render for ShowTarget<X> {
5038    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5039        match self {
5040            Self::Tables {
5041                extended,
5042                full,
5043                all,
5044                from,
5045                filter,
5046                ..
5047            } => {
5048                f.write_str("SHOW ")?;
5049                if *extended {
5050                    f.write_str("EXTENDED ")?;
5051                }
5052                if *full {
5053                    f.write_str("FULL ")?;
5054                }
5055                if *all {
5056                    f.write_str("ALL ")?;
5057                }
5058                f.write_str("TABLES")?;
5059                if let Some(from) = from {
5060                    f.write_str(" ")?;
5061                    from.render(ctx, f)?;
5062                }
5063                if let Some(filter) = filter {
5064                    f.write_str(" ")?;
5065                    filter.render(ctx, f)?;
5066                }
5067                Ok(())
5068            }
5069            Self::Columns {
5070                extended,
5071                full,
5072                spelling,
5073                table,
5074                database,
5075                filter,
5076                ..
5077            } => {
5078                f.write_str("SHOW ")?;
5079                if *extended {
5080                    f.write_str("EXTENDED ")?;
5081                }
5082                if *full {
5083                    f.write_str("FULL ")?;
5084                }
5085                f.write_str(match spelling {
5086                    ShowColumnsSpelling::Columns => "COLUMNS ",
5087                    ShowColumnsSpelling::Fields => "FIELDS ",
5088                })?;
5089                table.render(ctx, f)?;
5090                if let Some(database) = database {
5091                    f.write_str(" ")?;
5092                    database.render(ctx, f)?;
5093                }
5094                if let Some(filter) = filter {
5095                    f.write_str(" ")?;
5096                    filter.render(ctx, f)?;
5097                }
5098                Ok(())
5099            }
5100            Self::Create {
5101                kind,
5102                name,
5103                if_not_exists,
5104                ..
5105            } => {
5106                f.write_str("SHOW CREATE ")?;
5107                f.write_str(match kind {
5108                    ShowCreateKind::Table => "TABLE ",
5109                    ShowCreateKind::View => "VIEW ",
5110                    ShowCreateKind::Database { schema: false } => "DATABASE ",
5111                    ShowCreateKind::Database { schema: true } => "SCHEMA ",
5112                    ShowCreateKind::Event => "EVENT ",
5113                    ShowCreateKind::Procedure => "PROCEDURE ",
5114                    ShowCreateKind::Function => "FUNCTION ",
5115                    ShowCreateKind::Trigger => "TRIGGER ",
5116                })?;
5117                if *if_not_exists {
5118                    f.write_str("IF NOT EXISTS ")?;
5119                }
5120                name.render(ctx, f)
5121            }
5122            Self::Functions {
5123                kind, from, filter, ..
5124            } => {
5125                f.write_str("SHOW ")?;
5126                if let Some(kind) = kind {
5127                    f.write_str(match kind {
5128                        ShowFunctionsScope::User => "USER ",
5129                        ShowFunctionsScope::System => "SYSTEM ",
5130                        ShowFunctionsScope::All => "ALL ",
5131                    })?;
5132                }
5133                f.write_str("FUNCTIONS")?;
5134                if let Some(from) = from {
5135                    f.write_str(" ")?;
5136                    from.render(ctx, f)?;
5137                }
5138                if let Some(filter) = filter {
5139                    f.write_str(" ")?;
5140                    filter.render(ctx, f)?;
5141                }
5142                Ok(())
5143            }
5144            Self::RoutineStatus { kind, filter, .. } => {
5145                f.write_str(match kind {
5146                    ShowRoutineKind::Function => "SHOW FUNCTION STATUS",
5147                    ShowRoutineKind::Procedure => "SHOW PROCEDURE STATUS",
5148                })?;
5149                if let Some(filter) = filter {
5150                    f.write_str(" ")?;
5151                    filter.render(ctx, f)?;
5152                }
5153                Ok(())
5154            }
5155            Self::Listing {
5156                kind, from, filter, ..
5157            } => {
5158                f.write_str("SHOW ")?;
5159                match kind {
5160                    ShowListing::Databases { schemas } => {
5161                        f.write_str(if *schemas { "SCHEMAS" } else { "DATABASES" })?;
5162                    }
5163                    ShowListing::CharacterSet { charset } => {
5164                        f.write_str(if *charset { "CHARSET" } else { "CHARACTER SET" })?;
5165                    }
5166                    ShowListing::Collation => f.write_str("COLLATION")?,
5167                    ShowListing::Status { scope } => {
5168                        render_show_scope(*scope, f)?;
5169                        f.write_str("STATUS")?;
5170                    }
5171                    ShowListing::Variables { scope } => {
5172                        render_show_scope(*scope, f)?;
5173                        f.write_str("VARIABLES")?;
5174                    }
5175                    ShowListing::Events => f.write_str("EVENTS")?,
5176                    ShowListing::TableStatus => f.write_str("TABLE STATUS")?,
5177                    ShowListing::OpenTables => f.write_str("OPEN TABLES")?,
5178                    ShowListing::Triggers { full } => {
5179                        if *full {
5180                            f.write_str("FULL ")?;
5181                        }
5182                        f.write_str("TRIGGERS")?;
5183                    }
5184                }
5185                if let Some(from) = from {
5186                    f.write_str(" ")?;
5187                    from.render(ctx, f)?;
5188                }
5189                if let Some(filter) = filter {
5190                    f.write_str(" ")?;
5191                    filter.render(ctx, f)?;
5192                }
5193                Ok(())
5194            }
5195            Self::Bare { kind, .. } => {
5196                f.write_str("SHOW ")?;
5197                f.write_str(match kind {
5198                    ShowBare::Plugins => "PLUGINS",
5199                    ShowBare::Engines { storage: true } => "STORAGE ENGINES",
5200                    ShowBare::Engines { storage: false } => "ENGINES",
5201                    ShowBare::Privileges => "PRIVILEGES",
5202                    ShowBare::Profiles => "PROFILES",
5203                    ShowBare::Processlist { full: true } => "FULL PROCESSLIST",
5204                    ShowBare::Processlist { full: false } => "PROCESSLIST",
5205                    ShowBare::BinaryLogs => "BINARY LOGS",
5206                    ShowBare::Replicas => "REPLICAS",
5207                    ShowBare::BinaryLogStatus => "BINARY LOG STATUS",
5208                })
5209            }
5210            Self::Index {
5211                spelling,
5212                extended,
5213                table,
5214                database,
5215                filter,
5216                ..
5217            } => {
5218                f.write_str("SHOW ")?;
5219                if *extended {
5220                    f.write_str("EXTENDED ")?;
5221                }
5222                f.write_str(match spelling {
5223                    ShowIndexSpelling::Index => "INDEX ",
5224                    ShowIndexSpelling::Indexes => "INDEXES ",
5225                    ShowIndexSpelling::Keys => "KEYS ",
5226                })?;
5227                table.render(ctx, f)?;
5228                if let Some(database) = database {
5229                    f.write_str(" ")?;
5230                    database.render(ctx, f)?;
5231                }
5232                if let Some(filter) = filter {
5233                    f.write_str(" ")?;
5234                    filter.render(ctx, f)?;
5235                }
5236                Ok(())
5237            }
5238            Self::Engine {
5239                engine, artifact, ..
5240            } => {
5241                f.write_str("SHOW ENGINE ")?;
5242                match engine {
5243                    Some(name) => name.render(ctx, f)?,
5244                    None => f.write_str("ALL")?,
5245                }
5246                f.write_str(match artifact {
5247                    ShowEngineArtifact::Status => " STATUS",
5248                    ShowEngineArtifact::Mutex => " MUTEX",
5249                    ShowEngineArtifact::Logs => " LOGS",
5250                })
5251            }
5252            Self::ReplicaStatus { channel, .. } => {
5253                f.write_str("SHOW REPLICA STATUS")?;
5254                if let Some(channel) = channel {
5255                    f.write_str(" FOR CHANNEL ")?;
5256                    channel.render(ctx, f)?;
5257                }
5258                Ok(())
5259            }
5260            Self::Diagnostics {
5261                kind, count, limit, ..
5262            } => {
5263                f.write_str("SHOW ")?;
5264                if *count {
5265                    f.write_str("COUNT(*) ")?;
5266                }
5267                f.write_str(match kind {
5268                    ShowDiagnosticKind::Warnings => "WARNINGS",
5269                    ShowDiagnosticKind::Errors => "ERRORS",
5270                })?;
5271                if let Some(limit) = limit {
5272                    f.write_str(" ")?;
5273                    render_show_limit(limit, ctx, f)?;
5274                }
5275                Ok(())
5276            }
5277            Self::RoutineCode { kind, name, .. } => {
5278                f.write_str(match kind {
5279                    ShowRoutineKind::Function => "SHOW FUNCTION CODE ",
5280                    ShowRoutineKind::Procedure => "SHOW PROCEDURE CODE ",
5281                })?;
5282                name.render(ctx, f)
5283            }
5284            Self::Grants {
5285                user, using_roles, ..
5286            } => {
5287                f.write_str("SHOW GRANTS")?;
5288                if let Some(user) = user {
5289                    f.write_str(" FOR ")?;
5290                    user.render(ctx, f)?;
5291                    if !using_roles.is_empty() {
5292                        f.write_str(" USING ")?;
5293                        render_comma_separated(using_roles, ctx, f)?;
5294                    }
5295                }
5296                Ok(())
5297            }
5298            Self::CreateUser { user, .. } => {
5299                f.write_str("SHOW CREATE USER ")?;
5300                user.render(ctx, f)
5301            }
5302            Self::Profile {
5303                types,
5304                query,
5305                limit,
5306                ..
5307            } => {
5308                f.write_str("SHOW PROFILE")?;
5309                for (index, ty) in types.iter().enumerate() {
5310                    f.write_str(if index == 0 { " " } else { ", " })?;
5311                    f.write_str(ty.keyword())?;
5312                }
5313                if let Some(query) = query {
5314                    f.write_str(" FOR QUERY ")?;
5315                    query.render(ctx, f)?;
5316                }
5317                if let Some(limit) = limit {
5318                    f.write_str(" ")?;
5319                    render_show_limit(limit, ctx, f)?;
5320                }
5321                Ok(())
5322            }
5323            Self::LogEvents {
5324                relay,
5325                log_name,
5326                position,
5327                limit,
5328                channel,
5329                ..
5330            } => {
5331                f.write_str(if *relay {
5332                    "SHOW RELAYLOG EVENTS"
5333                } else {
5334                    "SHOW BINLOG EVENTS"
5335                })?;
5336                if let Some(log_name) = log_name {
5337                    f.write_str(" IN ")?;
5338                    log_name.render(ctx, f)?;
5339                }
5340                if let Some(position) = position {
5341                    f.write_str(" FROM ")?;
5342                    position.render(ctx, f)?;
5343                }
5344                if let Some(limit) = limit {
5345                    f.write_str(" ")?;
5346                    render_show_limit(limit, ctx, f)?;
5347                }
5348                if let Some(channel) = channel {
5349                    f.write_str(" FOR CHANNEL ")?;
5350                    channel.render(ctx, f)?;
5351                }
5352                Ok(())
5353            }
5354        }
5355    }
5356}
5357
5358impl ShowProfileType {
5359    /// The canonical keyword spelling of this profile type.
5360    fn keyword(self) -> &'static str {
5361        match self {
5362            Self::All => "ALL",
5363            Self::BlockIo => "BLOCK IO",
5364            Self::ContextSwitches => "CONTEXT SWITCHES",
5365            Self::Cpu => "CPU",
5366            Self::Ipc => "IPC",
5367            Self::Memory => "MEMORY",
5368            Self::PageFaults => "PAGE FAULTS",
5369            Self::Source => "SOURCE",
5370            Self::Swaps => "SWAPS",
5371        }
5372    }
5373}
5374
5375/// Render a [`ShowLimit`] — the shared `SHOW`-family `LIMIT` tail — as `LIMIT <row_count>`,
5376/// `LIMIT <offset>, <row_count>`, or `LIMIT <row_count> OFFSET <offset>`, matching the written
5377/// offset form. No surrounding whitespace; the caller writes the leading space.
5378fn render_show_limit(
5379    limit: &ShowLimit,
5380    ctx: &RenderCtx<'_>,
5381    f: &mut fmt::Formatter<'_>,
5382) -> fmt::Result {
5383    f.write_str("LIMIT ")?;
5384    match &limit.offset {
5385        Some(offset) if limit.offset_keyword => {
5386            limit.row_count.render(ctx, f)?;
5387            f.write_str(" OFFSET ")?;
5388            offset.render(ctx, f)
5389        }
5390        Some(offset) => {
5391            offset.render(ctx, f)?;
5392            f.write_str(", ")?;
5393            limit.row_count.render(ctx, f)
5394        }
5395        None => limit.row_count.render(ctx, f),
5396    }
5397}
5398
5399/// Render an optional `GLOBAL`/`SESSION`/`LOCAL` scope keyword (trailing space included) for
5400/// the `SHOW … STATUS` / `SHOW … VARIABLES` listings; nothing when the scope is `None`.
5401fn render_show_scope(scope: Option<ShowScope>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5402    if let Some(scope) = scope {
5403        f.write_str(match scope {
5404            ShowScope::Global => "GLOBAL ",
5405            ShowScope::Session => "SESSION ",
5406            ShowScope::Local => "LOCAL ",
5407        })?;
5408    }
5409    Ok(())
5410}
5411
5412impl Render for ShowFunctionsFilter {
5413    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5414        match self {
5415            Self::Name { like, name, .. } => {
5416                if *like {
5417                    f.write_str("LIKE ")?;
5418                }
5419                name.render(ctx, f)
5420            }
5421            Self::Regex { like, pattern, .. } => {
5422                if *like {
5423                    f.write_str("LIKE ")?;
5424                }
5425                pattern.render(ctx, f)
5426            }
5427        }
5428    }
5429}
5430
5431impl Render for ShowFrom {
5432    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5433        f.write_str(match self.keyword {
5434            ShowFromKeyword::From => "FROM ",
5435            ShowFromKeyword::In => "IN ",
5436        })?;
5437        self.name.render(ctx, f)
5438    }
5439}
5440
5441impl<X: Extension + Render> Render for ShowFilter<X> {
5442    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5443        match self {
5444            Self::Like { pattern, .. } => {
5445                f.write_str("LIKE ")?;
5446                pattern.render(ctx, f)
5447            }
5448            Self::Where { predicate, .. } => {
5449                f.write_str("WHERE ")?;
5450                predicate.render(ctx, f)
5451            }
5452        }
5453    }
5454}
5455
5456impl<X: Extension + Render> Render for KillStatement<X> {
5457    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5458        f.write_str("KILL")?;
5459        match self.target {
5460            KillTarget::Unspecified => {}
5461            KillTarget::Connection => f.write_str(" CONNECTION")?,
5462            KillTarget::Query => f.write_str(" QUERY")?,
5463        }
5464        f.write_str(" ")?;
5465        self.id.render(ctx, f)
5466    }
5467}
5468
5469impl<X: Extension + Render> Render for InstallStatement<X> {
5470    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5471        match self {
5472            InstallStatement::Plugin { name, soname, .. } => {
5473                f.write_str("INSTALL PLUGIN ")?;
5474                name.render(ctx, f)?;
5475                f.write_str(" SONAME ")?;
5476                soname.render(ctx, f)
5477            }
5478            InstallStatement::Component { urns, set, .. } => {
5479                f.write_str("INSTALL COMPONENT ")?;
5480                render_comma_separated(urns, ctx, f)?;
5481                if !set.is_empty() {
5482                    f.write_str(" SET ")?;
5483                    render_comma_separated(set, ctx, f)?;
5484                }
5485                Ok(())
5486            }
5487        }
5488    }
5489}
5490
5491impl Render for UninstallStatement {
5492    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5493        match self {
5494            UninstallStatement::Plugin { name, .. } => {
5495                f.write_str("UNINSTALL PLUGIN ")?;
5496                name.render(ctx, f)
5497            }
5498            UninstallStatement::Component { urns, .. } => {
5499                f.write_str("UNINSTALL COMPONENT ")?;
5500                render_comma_separated(urns, ctx, f)
5501            }
5502        }
5503    }
5504}
5505
5506impl<X: Extension + Render> Render for InstallComponentSetElement<X> {
5507    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5508        match self.scope {
5509            None => {}
5510            Some(InstallComponentSetScope::Global) => f.write_str("GLOBAL ")?,
5511            Some(InstallComponentSetScope::Persist) => f.write_str("PERSIST ")?,
5512        }
5513        self.name.render(ctx, f)?;
5514        render_mysql_set_assignment(self.assignment, ctx, f)?;
5515        self.value.render(ctx, f)
5516    }
5517}
5518
5519impl<X: Extension + Render> Render for InstallComponentSetValue<X> {
5520    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5521        match self {
5522            InstallComponentSetValue::On { .. } => f.write_str("ON"),
5523            InstallComponentSetValue::Expr { expr, .. } => expr.render(ctx, f),
5524        }
5525    }
5526}
5527
5528impl<X: Extension + Render> Render for HandlerStatement<X> {
5529    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5530        f.write_str("HANDLER ")?;
5531        self.table.render(ctx, f)?;
5532        f.write_str(" ")?;
5533        self.operation.render(ctx, f)
5534    }
5535}
5536
5537impl<X: Extension + Render> Render for HandlerOperation<X> {
5538    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5539        match self {
5540            HandlerOperation::Open {
5541                alias, as_keyword, ..
5542            } => {
5543                f.write_str("OPEN")?;
5544                if let Some(alias) = alias {
5545                    // `AS` is optional noise; the tag round-trips whichever was written.
5546                    f.write_str(if *as_keyword { " AS " } else { " " })?;
5547                    alias.render(ctx, f)?;
5548                }
5549                Ok(())
5550            }
5551            HandlerOperation::Close { .. } => f.write_str("CLOSE"),
5552            HandlerOperation::Read {
5553                selector,
5554                selection,
5555                limit,
5556                ..
5557            } => {
5558                f.write_str("READ ")?;
5559                selector.render(ctx, f)?;
5560                if let Some(selection) = selection {
5561                    f.write_str(" WHERE ")?;
5562                    selection.render(ctx, f)?;
5563                }
5564                if let Some(limit) = limit {
5565                    f.write_str(" ")?;
5566                    limit.render(ctx, f)?;
5567                }
5568                Ok(())
5569            }
5570        }
5571    }
5572}
5573
5574impl<X: Extension + Render> Render for HandlerReadSelector<X> {
5575    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5576        match self {
5577            HandlerReadSelector::Scan { direction, .. } => f.write_str(match direction {
5578                HandlerScanDirection::First => "FIRST",
5579                HandlerScanDirection::Next => "NEXT",
5580            }),
5581            HandlerReadSelector::Index {
5582                index, direction, ..
5583            } => {
5584                index.render(ctx, f)?;
5585                f.write_str(match direction {
5586                    HandlerIndexDirection::First => " FIRST",
5587                    HandlerIndexDirection::Next => " NEXT",
5588                    HandlerIndexDirection::Prev => " PREV",
5589                    HandlerIndexDirection::Last => " LAST",
5590                })
5591            }
5592            HandlerReadSelector::Key {
5593                index,
5594                comparison,
5595                key,
5596                ..
5597            } => {
5598                index.render(ctx, f)?;
5599                f.write_str(match comparison {
5600                    HandlerKeyComparison::Eq => " = (",
5601                    HandlerKeyComparison::GreaterOrEqual => " >= (",
5602                    HandlerKeyComparison::LessOrEqual => " <= (",
5603                    HandlerKeyComparison::Greater => " > (",
5604                    HandlerKeyComparison::Less => " < (",
5605                })?;
5606                render_comma_separated(key, ctx, f)?;
5607                f.write_str(")")
5608            }
5609        }
5610    }
5611}
5612
5613impl Render for CloneStatement {
5614    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5615        match self {
5616            CloneStatement::Local { data_directory, .. } => {
5617                f.write_str("CLONE LOCAL ")?;
5618                data_directory.render(ctx, f)
5619            }
5620            CloneStatement::Instance {
5621                source,
5622                port,
5623                password,
5624                data_directory,
5625                ssl,
5626                ..
5627            } => {
5628                f.write_str("CLONE INSTANCE FROM ")?;
5629                source.render(ctx, f)?;
5630                f.write_str(":")?;
5631                port.render(ctx, f)?;
5632                f.write_str(" IDENTIFIED BY ")?;
5633                password.render(ctx, f)?;
5634                if let Some(data_directory) = data_directory {
5635                    f.write_str(" ")?;
5636                    data_directory.render(ctx, f)?;
5637                }
5638                match ssl {
5639                    CloneSsl::Unspecified => {}
5640                    CloneSsl::Require => f.write_str(" REQUIRE SSL")?,
5641                    CloneSsl::RequireNo => f.write_str(" REQUIRE NO SSL")?,
5642                }
5643                Ok(())
5644            }
5645        }
5646    }
5647}
5648
5649impl Render for CloneDataDirectory {
5650    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5651        f.write_str(if self.equals {
5652            "DATA DIRECTORY = "
5653        } else {
5654            "DATA DIRECTORY "
5655        })?;
5656        self.path.render(ctx, f)
5657    }
5658}
5659
5660impl Render for ImportTableStatement {
5661    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5662        f.write_str("IMPORT TABLE FROM ")?;
5663        render_comma_separated(&self.files, ctx, f)
5664    }
5665}
5666
5667impl Render for HelpStatement {
5668    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5669        f.write_str("HELP ")?;
5670        self.topic.render(ctx, f)
5671    }
5672}
5673
5674impl Render for BinlogStatement {
5675    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5676        f.write_str("BINLOG ")?;
5677        self.event.render(ctx, f)
5678    }
5679}
5680
5681impl Render for PragmaStatement {
5682    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5683        f.write_str("PRAGMA ")?;
5684        self.name.render(ctx, f)?;
5685        if let Some(value) = &self.value {
5686            // The `parenthesized` surface tag picks the spelling; SQLite writes no
5687            // space before the call form's `(`.
5688            if self.parenthesized {
5689                f.write_str("(")?;
5690                value.render(ctx, f)?;
5691                f.write_str(")")?;
5692            } else {
5693                f.write_str(" = ")?;
5694                value.render(ctx, f)?;
5695            }
5696        }
5697        Ok(())
5698    }
5699}
5700
5701impl<X: Extension + Render> Render for AttachStatement<X> {
5702    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5703        f.write_str("ATTACH ")?;
5704        if self.database_keyword {
5705            f.write_str("DATABASE ")?;
5706        }
5707        self.target.render(ctx, f)?;
5708        f.write_str(" AS ")?;
5709        self.schema.render(ctx, f)
5710    }
5711}
5712
5713impl Render for DetachStatement {
5714    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5715        f.write_str("DETACH ")?;
5716        if self.database_keyword {
5717            f.write_str("DATABASE ")?;
5718        }
5719        if self.if_exists {
5720            f.write_str("IF EXISTS ")?;
5721        }
5722        self.schema.render(ctx, f)
5723    }
5724}
5725
5726impl Render for CheckpointStatement {
5727    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5728        if self.force {
5729            f.write_str("FORCE ")?;
5730        }
5731        f.write_str("CHECKPOINT")?;
5732        if let Some(database) = &self.database {
5733            f.write_str(" ")?;
5734            database.render(ctx, f)?;
5735        }
5736        Ok(())
5737    }
5738}
5739
5740impl Render for LoadStatement {
5741    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5742        f.write_str("LOAD ")?;
5743        self.target.render(ctx, f)
5744    }
5745}
5746
5747impl Render for LoadTarget {
5748    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5749        match self {
5750            Self::Name { name, .. } => name.render(ctx, f),
5751            Self::Path { path, .. } => path.render(ctx, f),
5752        }
5753    }
5754}
5755
5756impl Render for UseStatement {
5757    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5758        f.write_str("USE ")?;
5759        self.name.render(ctx, f)
5760    }
5761}
5762
5763impl Render for UpdateExtensionsStatement {
5764    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5765        f.write_str("UPDATE EXTENSIONS")?;
5766        // An empty list is the bare `UPDATE EXTENSIONS` (refresh all); a written list is
5767        // never empty (`UPDATE EXTENSIONS ()` is a DuckDB parser error), so a non-empty
5768        // vector renders the parenthesized form.
5769        if !self.extensions.is_empty() {
5770            f.write_str(" (")?;
5771            render_comma_separated(&self.extensions, ctx, f)?;
5772            f.write_str(")")?;
5773        }
5774        Ok(())
5775    }
5776}
5777
5778impl<X: Extension + Render> Render for VacuumStatement<X> {
5779    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5780        f.write_str("VACUUM")?;
5781        // DuckDB `ANALYZE` option precedes the operands; SQLite never sets it. The
5782        // parenthesized list canonicalizes to the single `(ANALYZE)` form (repeats carry
5783        // no meaning — see `VacuumAnalyze`).
5784        match self.analyze {
5785            Some(VacuumAnalyze::Keyword) => f.write_str(" ANALYZE")?,
5786            Some(VacuumAnalyze::Parenthesized) => f.write_str(" (ANALYZE)")?,
5787            None => {}
5788        }
5789        // The name operand: SQLite's single-ident schema or DuckDB's qualified table
5790        // (mutually exclusive by dialect).
5791        if let Some(schema) = &self.schema {
5792            f.write_str(" ")?;
5793            schema.render(ctx, f)?;
5794        }
5795        if let Some(table) = &self.table {
5796            f.write_str(" ")?;
5797            table.render(ctx, f)?;
5798        }
5799        // DuckDB column list, only alongside a table.
5800        if let Some(columns) = &self.columns {
5801            f.write_str(" (")?;
5802            render_comma_separated(columns, ctx, f)?;
5803            f.write_str(")")?;
5804        }
5805        // SQLite `INTO <expr>` tail.
5806        if let Some(into) = &self.into {
5807            f.write_str(" INTO ")?;
5808            into.render(ctx, f)?;
5809        }
5810        Ok(())
5811    }
5812}
5813
5814impl Render for ReindexStatement {
5815    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5816        f.write_str("REINDEX")?;
5817        if let Some(target) = &self.target {
5818            f.write_str(" ")?;
5819            target.render(ctx, f)?;
5820        }
5821        Ok(())
5822    }
5823}
5824
5825impl Render for AnalyzeStatement {
5826    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5827        f.write_str("ANALYZE")?;
5828        if let Some(target) = &self.target {
5829            f.write_str(" ")?;
5830            target.render(ctx, f)?;
5831        }
5832        // DuckDB column list, only alongside a target.
5833        if let Some(columns) = &self.columns {
5834            f.write_str(" (")?;
5835            render_comma_separated(columns, ctx, f)?;
5836            f.write_str(")")?;
5837        }
5838        Ok(())
5839    }
5840}
5841
5842impl Render for TableMaintenanceStatement {
5843    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5844        // Verb + the optional `NO_WRITE_TO_BINLOG | LOCAL` prefix (ANALYZE/OPTIMIZE/REPAIR).
5845        let (verb, prefix) = match &self.kind {
5846            TableMaintenanceKind::Analyze {
5847                no_write_to_binlog, ..
5848            } => ("ANALYZE", *no_write_to_binlog),
5849            TableMaintenanceKind::Check { .. } => ("CHECK", None),
5850            TableMaintenanceKind::Checksum { .. } => ("CHECKSUM", None),
5851            TableMaintenanceKind::Optimize {
5852                no_write_to_binlog, ..
5853            } => ("OPTIMIZE", *no_write_to_binlog),
5854            TableMaintenanceKind::Repair {
5855                no_write_to_binlog, ..
5856            } => ("REPAIR", *no_write_to_binlog),
5857        };
5858        f.write_str(verb)?;
5859        if let Some(prefix) = prefix {
5860            f.write_str(" ")?;
5861            f.write_str(no_write_to_binlog_keyword(prefix))?;
5862        }
5863        f.write_str(" ")?;
5864        f.write_str(table_keyword_str(self.table_keyword))?;
5865        f.write_str(" ")?;
5866        render_comma_separated(&self.tables, ctx, f)?;
5867        // The per-verb trailing options.
5868        match &self.kind {
5869            TableMaintenanceKind::Analyze {
5870                histogram: Some(histogram),
5871                ..
5872            } => {
5873                f.write_str(" ")?;
5874                histogram.render(ctx, f)?;
5875            }
5876            TableMaintenanceKind::Check { options, .. } => {
5877                for option in options {
5878                    f.write_str(" ")?;
5879                    f.write_str(check_table_option_keyword(*option))?;
5880                }
5881            }
5882            TableMaintenanceKind::Checksum {
5883                option: Some(option),
5884                ..
5885            } => {
5886                f.write_str(" ")?;
5887                f.write_str(checksum_table_option_keyword(*option))?;
5888            }
5889            TableMaintenanceKind::Repair { options, .. } => {
5890                for option in options {
5891                    f.write_str(" ")?;
5892                    f.write_str(repair_table_option_keyword(*option))?;
5893                }
5894            }
5895            TableMaintenanceKind::Analyze {
5896                histogram: None, ..
5897            }
5898            | TableMaintenanceKind::Checksum { option: None, .. }
5899            | TableMaintenanceKind::Optimize { .. } => {}
5900        }
5901        Ok(())
5902    }
5903}
5904
5905impl Render for AnalyzeHistogram {
5906    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5907        match self {
5908            AnalyzeHistogram::Update {
5909                columns, buckets, ..
5910            } => {
5911                f.write_str("UPDATE HISTOGRAM ON ")?;
5912                render_comma_separated(columns, ctx, f)?;
5913                if let Some(buckets) = buckets {
5914                    f.write_str(" WITH ")?;
5915                    buckets.render(ctx, f)?;
5916                    f.write_str(" BUCKETS")?;
5917                }
5918            }
5919            AnalyzeHistogram::Drop { columns, .. } => {
5920                f.write_str("DROP HISTOGRAM ON ")?;
5921                render_comma_separated(columns, ctx, f)?;
5922            }
5923        }
5924        Ok(())
5925    }
5926}
5927
5928impl Render for CacheIndexStatement {
5929    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5930        f.write_str("CACHE INDEX ")?;
5931        self.targets.render(ctx, f)?;
5932        f.write_str(" IN ")?;
5933        self.cache.render(ctx, f)
5934    }
5935}
5936
5937impl Render for CacheIndexTargets {
5938    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5939        match self {
5940            CacheIndexTargets::Tables { tables, .. } => render_comma_separated(tables, ctx, f),
5941            CacheIndexTargets::Partition {
5942                table,
5943                partition,
5944                keys,
5945                ..
5946            } => {
5947                table.render(ctx, f)?;
5948                f.write_str(" ")?;
5949                partition.render(ctx, f)?;
5950                if let Some(keys) = keys {
5951                    f.write_str(" ")?;
5952                    keys.render(ctx, f)?;
5953                }
5954                Ok(())
5955            }
5956        }
5957    }
5958}
5959
5960impl Render for CacheIndexTable {
5961    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5962        self.table.render(ctx, f)?;
5963        if let Some(keys) = &self.keys {
5964            f.write_str(" ")?;
5965            keys.render(ctx, f)?;
5966        }
5967        Ok(())
5968    }
5969}
5970
5971impl Render for LoadIndexStatement {
5972    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5973        f.write_str("LOAD INDEX INTO CACHE ")?;
5974        self.targets.render(ctx, f)
5975    }
5976}
5977
5978impl Render for LoadIndexTargets {
5979    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5980        match self {
5981            LoadIndexTargets::Tables { tables, .. } => render_comma_separated(tables, ctx, f),
5982            LoadIndexTargets::Partition {
5983                table,
5984                partition,
5985                keys,
5986                ignore_leaves,
5987                ..
5988            } => {
5989                table.render(ctx, f)?;
5990                f.write_str(" ")?;
5991                partition.render(ctx, f)?;
5992                if let Some(keys) = keys {
5993                    f.write_str(" ")?;
5994                    keys.render(ctx, f)?;
5995                }
5996                if *ignore_leaves {
5997                    f.write_str(" IGNORE LEAVES")?;
5998                }
5999                Ok(())
6000            }
6001        }
6002    }
6003}
6004
6005impl Render for LoadIndexTable {
6006    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6007        self.table.render(ctx, f)?;
6008        if let Some(keys) = &self.keys {
6009            f.write_str(" ")?;
6010            keys.render(ctx, f)?;
6011        }
6012        if self.ignore_leaves {
6013            f.write_str(" IGNORE LEAVES")?;
6014        }
6015        Ok(())
6016    }
6017}
6018
6019impl Render for CacheIndexKeyList {
6020    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6021        f.write_str(match self.keyword {
6022            CacheIndexKeyword::Index => "INDEX",
6023            CacheIndexKeyword::Key => "KEY",
6024        })?;
6025        f.write_str(" (")?;
6026        render_comma_separated(&self.keys, ctx, f)?;
6027        f.write_str(")")
6028    }
6029}
6030
6031impl Render for KeyCacheName {
6032    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6033        match self {
6034            KeyCacheName::Named { name, .. } => name.render(ctx, f),
6035            KeyCacheName::Default { .. } => f.write_str("DEFAULT"),
6036        }
6037    }
6038}
6039
6040impl Render for PartitionSelection {
6041    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6042        match self {
6043            PartitionSelection::All { .. } => f.write_str("PARTITION (ALL)"),
6044            PartitionSelection::Names { names, .. } => {
6045                f.write_str("PARTITION (")?;
6046                render_comma_separated(names, ctx, f)?;
6047                f.write_str(")")
6048            }
6049        }
6050    }
6051}
6052
6053impl Render for RenameStatement {
6054    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6055        match self {
6056            RenameStatement::Table {
6057                table_keyword,
6058                renames,
6059                ..
6060            } => {
6061                f.write_str("RENAME ")?;
6062                f.write_str(table_keyword_str(*table_keyword))?;
6063                f.write_str(" ")?;
6064                render_comma_separated(renames, ctx, f)
6065            }
6066            RenameStatement::User { renames, .. } => {
6067                f.write_str("RENAME USER ")?;
6068                render_comma_separated(renames, ctx, f)
6069            }
6070        }
6071    }
6072}
6073
6074impl Render for TableRename {
6075    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6076        self.from.render(ctx, f)?;
6077        f.write_str(" TO ")?;
6078        self.to.render(ctx, f)
6079    }
6080}
6081
6082impl Render for UserRename {
6083    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6084        self.from.render(ctx, f)?;
6085        f.write_str(" TO ")?;
6086        self.to.render(ctx, f)
6087    }
6088}
6089
6090impl Render for FlushStatement {
6091    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6092        f.write_str("FLUSH")?;
6093        if let Some(prefix) = self.no_write_to_binlog {
6094            f.write_str(" ")?;
6095            f.write_str(no_write_to_binlog_keyword(prefix))?;
6096        }
6097        f.write_str(" ")?;
6098        self.target.render(ctx, f)
6099    }
6100}
6101
6102impl Render for FlushTarget {
6103    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6104        match self {
6105            FlushTarget::Tables {
6106                table_keyword,
6107                tables,
6108                lock,
6109                ..
6110            } => {
6111                f.write_str(table_keyword_str(*table_keyword))?;
6112                if !tables.is_empty() {
6113                    f.write_str(" ")?;
6114                    render_comma_separated(tables, ctx, f)?;
6115                }
6116                if let Some(lock) = lock {
6117                    f.write_str(" ")?;
6118                    f.write_str(match lock {
6119                        FlushTablesLock::WithReadLock => "WITH READ LOCK",
6120                        FlushTablesLock::ForExport => "FOR EXPORT",
6121                    })?;
6122                }
6123                Ok(())
6124            }
6125            FlushTarget::Options { options, .. } => render_comma_separated(options, ctx, f),
6126        }
6127    }
6128}
6129
6130impl Render for FlushOption {
6131    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6132        match self {
6133            FlushOption::Privileges { .. } => f.write_str("PRIVILEGES"),
6134            FlushOption::Logs { .. } => f.write_str("LOGS"),
6135            FlushOption::BinaryLogs { .. } => f.write_str("BINARY LOGS"),
6136            FlushOption::EngineLogs { .. } => f.write_str("ENGINE LOGS"),
6137            FlushOption::ErrorLogs { .. } => f.write_str("ERROR LOGS"),
6138            FlushOption::GeneralLogs { .. } => f.write_str("GENERAL LOGS"),
6139            FlushOption::SlowLogs { .. } => f.write_str("SLOW LOGS"),
6140            FlushOption::RelayLogs { channel, .. } => {
6141                f.write_str("RELAY LOGS")?;
6142                if let Some(channel) = channel {
6143                    f.write_str(" FOR CHANNEL ")?;
6144                    channel.render(ctx, f)?;
6145                }
6146                Ok(())
6147            }
6148            FlushOption::Status { .. } => f.write_str("STATUS"),
6149            FlushOption::UserResources { .. } => f.write_str("USER_RESOURCES"),
6150            FlushOption::OptimizerCosts { .. } => f.write_str("OPTIMIZER_COSTS"),
6151        }
6152    }
6153}
6154
6155impl<X: Extension + Render> Render for PurgeStatement<X> {
6156    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6157        f.write_str("PURGE BINARY LOGS ")?;
6158        self.target.render(ctx, f)
6159    }
6160}
6161
6162impl<X: Extension + Render> Render for PurgeTarget<X> {
6163    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6164        match self {
6165            PurgeTarget::To { log, .. } => {
6166                f.write_str("TO ")?;
6167                log.render(ctx, f)
6168            }
6169            PurgeTarget::Before { datetime, .. } => {
6170                f.write_str("BEFORE ")?;
6171                datetime.render(ctx, f)
6172            }
6173        }
6174    }
6175}
6176
6177/// Render a trailing `[FOR CHANNEL '<name>']` suffix, shared by the four replication verbs
6178/// that carry one.
6179fn render_for_channel(
6180    channel: &Option<Literal>,
6181    ctx: &RenderCtx<'_>,
6182    f: &mut fmt::Formatter<'_>,
6183) -> fmt::Result {
6184    if let Some(channel) = channel {
6185        f.write_str(" FOR CHANNEL ")?;
6186        channel.render(ctx, f)?;
6187    }
6188    Ok(())
6189}
6190
6191/// Render one fixed-position `START REPLICA` connection option (`keyword` includes its
6192/// leading space and trailing ` = `), if present.
6193fn render_replica_connection_option(
6194    keyword: &str,
6195    value: &Option<Literal>,
6196    ctx: &RenderCtx<'_>,
6197    f: &mut fmt::Formatter<'_>,
6198) -> fmt::Result {
6199    if let Some(value) = value {
6200        f.write_str(keyword)?;
6201        value.render(ctx, f)?;
6202    }
6203    Ok(())
6204}
6205
6206impl Render for ReplicationStatement {
6207    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6208        match self {
6209            ReplicationStatement::ChangeSource {
6210                options, channel, ..
6211            } => {
6212                f.write_str("CHANGE REPLICATION SOURCE TO ")?;
6213                render_comma_separated(options, ctx, f)?;
6214                render_for_channel(channel, ctx, f)
6215            }
6216            ReplicationStatement::ChangeFilter { rules, channel, .. } => {
6217                f.write_str("CHANGE REPLICATION FILTER ")?;
6218                render_comma_separated(rules, ctx, f)?;
6219                render_for_channel(channel, ctx, f)
6220            }
6221            ReplicationStatement::StartReplica {
6222                threads,
6223                until,
6224                user,
6225                password,
6226                default_auth,
6227                plugin_dir,
6228                channel,
6229                ..
6230            } => {
6231                f.write_str("START REPLICA")?;
6232                if !threads.is_empty() {
6233                    f.write_str(" ")?;
6234                    render_comma_separated(threads, ctx, f)?;
6235                }
6236                if !until.is_empty() {
6237                    f.write_str(" UNTIL ")?;
6238                    render_comma_separated(until, ctx, f)?;
6239                }
6240                render_replica_connection_option(" USER = ", user, ctx, f)?;
6241                render_replica_connection_option(" PASSWORD = ", password, ctx, f)?;
6242                render_replica_connection_option(" DEFAULT_AUTH = ", default_auth, ctx, f)?;
6243                render_replica_connection_option(" PLUGIN_DIR = ", plugin_dir, ctx, f)?;
6244                render_for_channel(channel, ctx, f)
6245            }
6246            ReplicationStatement::StopReplica {
6247                threads, channel, ..
6248            } => {
6249                f.write_str("STOP REPLICA")?;
6250                if !threads.is_empty() {
6251                    f.write_str(" ")?;
6252                    render_comma_separated(threads, ctx, f)?;
6253                }
6254                render_for_channel(channel, ctx, f)
6255            }
6256            ReplicationStatement::StartGroupReplication { options, .. } => {
6257                f.write_str("START GROUP_REPLICATION")?;
6258                if !options.is_empty() {
6259                    f.write_str(" ")?;
6260                    render_comma_separated(options, ctx, f)?;
6261                }
6262                Ok(())
6263            }
6264            ReplicationStatement::StopGroupReplication { .. } => {
6265                f.write_str("STOP GROUP_REPLICATION")
6266            }
6267        }
6268    }
6269}
6270
6271impl Render for ChangeReplicationSourceOption {
6272    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6273        f.write_str(self.name.keyword())?;
6274        f.write_str(" = ")?;
6275        self.value.render(ctx, f)
6276    }
6277}
6278
6279impl Render for ChangeReplicationSourceOptionValue {
6280    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6281        match self {
6282            Self::String { value, .. } | Self::Number { value, .. } => value.render(ctx, f),
6283            Self::NullableString { value, .. } => match value {
6284                Some(value) => value.render(ctx, f),
6285                None => f.write_str("NULL"),
6286            },
6287            Self::User { account, .. } => match account {
6288                Some(account) => account.render(ctx, f),
6289                None => f.write_str("NULL"),
6290            },
6291            Self::ServerIds { ids, .. } => {
6292                f.write_str("(")?;
6293                render_comma_separated(ids, ctx, f)?;
6294                f.write_str(")")
6295            }
6296            Self::PrimaryKeyCheck { check, .. } => f.write_str(match check {
6297                RequirePrimaryKeyCheck::On => "ON",
6298                RequirePrimaryKeyCheck::Off => "OFF",
6299                RequirePrimaryKeyCheck::Stream => "STREAM",
6300                RequirePrimaryKeyCheck::Generate => "GENERATE",
6301            }),
6302            Self::AssignGtids { kind, uuid, .. } => match kind {
6303                AssignGtidsKind::Off => f.write_str("OFF"),
6304                AssignGtidsKind::Local => f.write_str("LOCAL"),
6305                AssignGtidsKind::Uuid => match uuid {
6306                    Some(uuid) => uuid.render(ctx, f),
6307                    None => Ok(()),
6308                },
6309            },
6310        }
6311    }
6312}
6313
6314impl Render for ReplicationFilterRule {
6315    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6316        match self {
6317            Self::DoDb { databases, .. } => {
6318                f.write_str("REPLICATE_DO_DB = (")?;
6319                render_comma_separated(databases, ctx, f)?;
6320                f.write_str(")")
6321            }
6322            Self::IgnoreDb { databases, .. } => {
6323                f.write_str("REPLICATE_IGNORE_DB = (")?;
6324                render_comma_separated(databases, ctx, f)?;
6325                f.write_str(")")
6326            }
6327            Self::DoTable { tables, .. } => {
6328                f.write_str("REPLICATE_DO_TABLE = (")?;
6329                render_comma_separated(tables, ctx, f)?;
6330                f.write_str(")")
6331            }
6332            Self::IgnoreTable { tables, .. } => {
6333                f.write_str("REPLICATE_IGNORE_TABLE = (")?;
6334                render_comma_separated(tables, ctx, f)?;
6335                f.write_str(")")
6336            }
6337            Self::WildDoTable { patterns, .. } => {
6338                f.write_str("REPLICATE_WILD_DO_TABLE = (")?;
6339                render_comma_separated(patterns, ctx, f)?;
6340                f.write_str(")")
6341            }
6342            Self::WildIgnoreTable { patterns, .. } => {
6343                f.write_str("REPLICATE_WILD_IGNORE_TABLE = (")?;
6344                render_comma_separated(patterns, ctx, f)?;
6345                f.write_str(")")
6346            }
6347            Self::RewriteDb { pairs, .. } => {
6348                f.write_str("REPLICATE_REWRITE_DB = (")?;
6349                render_comma_separated(pairs, ctx, f)?;
6350                f.write_str(")")
6351            }
6352        }
6353    }
6354}
6355
6356impl Render for RewriteDbPair {
6357    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6358        f.write_str("(")?;
6359        self.from.render(ctx, f)?;
6360        f.write_str(", ")?;
6361        self.to.render(ctx, f)?;
6362        f.write_str(")")
6363    }
6364}
6365
6366impl Render for ReplicaThreadOption {
6367    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6368        f.write_str(match self {
6369            Self::Sql { .. } => "SQL_THREAD",
6370            Self::Io {
6371                keyword: IoThreadKeyword::Io,
6372                ..
6373            } => "IO_THREAD",
6374            Self::Io {
6375                keyword: IoThreadKeyword::Relay,
6376                ..
6377            } => "RELAY_THREAD",
6378        })
6379    }
6380}
6381
6382impl Render for ReplicaUntilCondition {
6383    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6384        match self {
6385            Self::SourceLogFile { value, .. } => {
6386                f.write_str("SOURCE_LOG_FILE = ")?;
6387                value.render(ctx, f)
6388            }
6389            Self::SourceLogPos { value, .. } => {
6390                f.write_str("SOURCE_LOG_POS = ")?;
6391                value.render(ctx, f)
6392            }
6393            Self::RelayLogFile { value, .. } => {
6394                f.write_str("RELAY_LOG_FILE = ")?;
6395                value.render(ctx, f)
6396            }
6397            Self::RelayLogPos { value, .. } => {
6398                f.write_str("RELAY_LOG_POS = ")?;
6399                value.render(ctx, f)
6400            }
6401            Self::SqlBeforeGtids { value, .. } => {
6402                f.write_str("SQL_BEFORE_GTIDS = ")?;
6403                value.render(ctx, f)
6404            }
6405            Self::SqlAfterGtids { value, .. } => {
6406                f.write_str("SQL_AFTER_GTIDS = ")?;
6407                value.render(ctx, f)
6408            }
6409            Self::SqlAfterMtsGaps { .. } => f.write_str("SQL_AFTER_MTS_GAPS"),
6410        }
6411    }
6412}
6413
6414impl Render for GroupReplicationOption {
6415    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6416        match self {
6417            Self::User { value, .. } => {
6418                f.write_str("USER = ")?;
6419                value.render(ctx, f)
6420            }
6421            Self::Password { value, .. } => {
6422                f.write_str("PASSWORD = ")?;
6423                value.render(ctx, f)
6424            }
6425            Self::DefaultAuth { value, .. } => {
6426                f.write_str("DEFAULT_AUTH = ")?;
6427                value.render(ctx, f)
6428            }
6429        }
6430    }
6431}
6432
6433impl Render for AccountName {
6434    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6435        match self {
6436            Self::Account { user, host, .. } => {
6437                user.render(ctx, f)?;
6438                if let Some(host) = host {
6439                    f.write_str("@")?;
6440                    host.render(ctx, f)?;
6441                }
6442                Ok(())
6443            }
6444            Self::CurrentUser { parens, .. } => {
6445                f.write_str("CURRENT_USER")?;
6446                if *parens {
6447                    f.write_str("()")?;
6448                }
6449                Ok(())
6450            }
6451        }
6452    }
6453}
6454
6455// --- User / role administration DDL render ---------------------------------------------
6456
6457/// The shared `[REQUIRE …] [WITH <resource> …] [<lock option> …] [<attribute>]` option tail of
6458/// `CREATE USER` and `ALTER USER … <list>` — a borrowing view over the four clauses, grouped so
6459/// [`render_user_option_tail`] takes one data parameter rather than four.
6460struct UserOptionTail<'a> {
6461    require: &'a Option<TlsRequirement>,
6462    resource_options: &'a [ResourceLimit],
6463    password_lock_options: &'a [PasswordLockOption],
6464    attribute: &'a Option<UserAttribute>,
6465}
6466
6467/// Render the shared option tail, each clause prefixed by one leading space so it abuts the
6468/// preceding account list.
6469fn render_user_option_tail(
6470    tail: UserOptionTail<'_>,
6471    ctx: &RenderCtx<'_>,
6472    f: &mut fmt::Formatter<'_>,
6473) -> fmt::Result {
6474    if let Some(require) = tail.require {
6475        f.write_str(" ")?;
6476        require.render(ctx, f)?;
6477    }
6478    if !tail.resource_options.is_empty() {
6479        // `WITH` is written once, ahead of a whitespace-separated option run.
6480        f.write_str(" WITH")?;
6481        for option in tail.resource_options {
6482            f.write_str(" ")?;
6483            option.render(ctx, f)?;
6484        }
6485    }
6486    for option in tail.password_lock_options {
6487        f.write_str(" ")?;
6488        option.render(ctx, f)?;
6489    }
6490    if let Some(attribute) = tail.attribute {
6491        f.write_str(" ")?;
6492        attribute.render(ctx, f)?;
6493    }
6494    Ok(())
6495}
6496
6497impl Render for CreateUser {
6498    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6499        f.write_str("CREATE USER ")?;
6500        if self.if_not_exists {
6501            f.write_str("IF NOT EXISTS ")?;
6502        }
6503        render_comma_separated(&self.users, ctx, f)?;
6504        if !self.default_roles.is_empty() {
6505            f.write_str(" DEFAULT ROLE ")?;
6506            render_comma_separated(&self.default_roles, ctx, f)?;
6507        }
6508        render_user_option_tail(
6509            UserOptionTail {
6510                require: &self.require,
6511                resource_options: &self.resource_options,
6512                password_lock_options: &self.password_lock_options,
6513                attribute: &self.attribute,
6514            },
6515            ctx,
6516            f,
6517        )
6518    }
6519}
6520
6521impl Render for UserSpec {
6522    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6523        self.account.render(ctx, f)?;
6524        if let Some(auth) = &self.auth {
6525            f.write_str(" ")?;
6526            auth.render(ctx, f)?;
6527        }
6528        Ok(())
6529    }
6530}
6531
6532impl Render for AuthOption {
6533    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6534        match self {
6535            Self::Password { password, .. } => {
6536                f.write_str("IDENTIFIED BY ")?;
6537                password.render(ctx, f)
6538            }
6539            Self::RandomPassword { .. } => f.write_str("IDENTIFIED BY RANDOM PASSWORD"),
6540            Self::Plugin { plugin, .. } => {
6541                f.write_str("IDENTIFIED WITH ")?;
6542                plugin.render(ctx, f)
6543            }
6544            Self::PluginAs {
6545                plugin,
6546                auth_string,
6547                ..
6548            } => {
6549                f.write_str("IDENTIFIED WITH ")?;
6550                plugin.render(ctx, f)?;
6551                f.write_str(" AS ")?;
6552                auth_string.render(ctx, f)
6553            }
6554            Self::PluginByPassword {
6555                plugin, password, ..
6556            } => {
6557                f.write_str("IDENTIFIED WITH ")?;
6558                plugin.render(ctx, f)?;
6559                f.write_str(" BY ")?;
6560                password.render(ctx, f)
6561            }
6562            Self::PluginByRandomPassword { plugin, .. } => {
6563                f.write_str("IDENTIFIED WITH ")?;
6564                plugin.render(ctx, f)?;
6565                f.write_str(" BY RANDOM PASSWORD")
6566            }
6567        }
6568    }
6569}
6570
6571impl Render for TlsRequirement {
6572    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6573        f.write_str("REQUIRE ")?;
6574        match self {
6575            Self::None { .. } => f.write_str("NONE"),
6576            Self::Ssl { .. } => f.write_str("SSL"),
6577            Self::X509 { .. } => f.write_str("X509"),
6578            Self::Options { options, .. } => {
6579                for (i, option) in options.iter().enumerate() {
6580                    if i > 0 {
6581                        f.write_str(" AND ")?;
6582                    }
6583                    option.render(ctx, f)?;
6584                }
6585                Ok(())
6586            }
6587        }
6588    }
6589}
6590
6591impl Render for TlsOption {
6592    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6593        match self {
6594            Self::Subject { value, .. } => {
6595                f.write_str("SUBJECT ")?;
6596                value.render(ctx, f)
6597            }
6598            Self::Issuer { value, .. } => {
6599                f.write_str("ISSUER ")?;
6600                value.render(ctx, f)
6601            }
6602            Self::Cipher { value, .. } => {
6603                f.write_str("CIPHER ")?;
6604                value.render(ctx, f)
6605            }
6606        }
6607    }
6608}
6609
6610impl Render for ResourceLimit {
6611    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6612        let (keyword, value) = match self {
6613            Self::MaxQueriesPerHour { value, .. } => ("MAX_QUERIES_PER_HOUR", value),
6614            Self::MaxUpdatesPerHour { value, .. } => ("MAX_UPDATES_PER_HOUR", value),
6615            Self::MaxConnectionsPerHour { value, .. } => ("MAX_CONNECTIONS_PER_HOUR", value),
6616            Self::MaxUserConnections { value, .. } => ("MAX_USER_CONNECTIONS", value),
6617        };
6618        f.write_str(keyword)?;
6619        f.write_str(" ")?;
6620        value.render(ctx, f)
6621    }
6622}
6623
6624impl Render for PasswordLockOption {
6625    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6626        match self {
6627            Self::AccountLock { .. } => f.write_str("ACCOUNT LOCK"),
6628            Self::AccountUnlock { .. } => f.write_str("ACCOUNT UNLOCK"),
6629            Self::PasswordExpire { .. } => f.write_str("PASSWORD EXPIRE"),
6630            Self::PasswordExpireDefault { .. } => f.write_str("PASSWORD EXPIRE DEFAULT"),
6631            Self::PasswordExpireNever { .. } => f.write_str("PASSWORD EXPIRE NEVER"),
6632            Self::PasswordExpireInterval { days, .. } => {
6633                f.write_str("PASSWORD EXPIRE INTERVAL ")?;
6634                days.render(ctx, f)?;
6635                f.write_str(" DAY")
6636            }
6637            Self::PasswordHistory { count, .. } => {
6638                f.write_str("PASSWORD HISTORY ")?;
6639                count.render(ctx, f)
6640            }
6641            Self::PasswordHistoryDefault { .. } => f.write_str("PASSWORD HISTORY DEFAULT"),
6642            Self::PasswordReuseInterval { days, .. } => {
6643                f.write_str("PASSWORD REUSE INTERVAL ")?;
6644                days.render(ctx, f)?;
6645                f.write_str(" DAY")
6646            }
6647            Self::PasswordReuseIntervalDefault { .. } => {
6648                f.write_str("PASSWORD REUSE INTERVAL DEFAULT")
6649            }
6650            Self::PasswordRequireCurrent { .. } => f.write_str("PASSWORD REQUIRE CURRENT"),
6651            Self::PasswordRequireCurrentDefault { .. } => {
6652                f.write_str("PASSWORD REQUIRE CURRENT DEFAULT")
6653            }
6654            Self::PasswordRequireCurrentOptional { .. } => {
6655                f.write_str("PASSWORD REQUIRE CURRENT OPTIONAL")
6656            }
6657            Self::FailedLoginAttempts { count, .. } => {
6658                f.write_str("FAILED_LOGIN_ATTEMPTS ")?;
6659                count.render(ctx, f)
6660            }
6661            Self::PasswordLockTime { days, .. } => {
6662                f.write_str("PASSWORD_LOCK_TIME ")?;
6663                days.render(ctx, f)
6664            }
6665            Self::PasswordLockTimeUnbounded { .. } => f.write_str("PASSWORD_LOCK_TIME UNBOUNDED"),
6666        }
6667    }
6668}
6669
6670impl Render for UserAttribute {
6671    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6672        match self {
6673            Self::Comment { comment, .. } => {
6674                f.write_str("COMMENT ")?;
6675                comment.render(ctx, f)
6676            }
6677            Self::Attribute { attribute, .. } => {
6678                f.write_str("ATTRIBUTE ")?;
6679                attribute.render(ctx, f)
6680            }
6681        }
6682    }
6683}
6684
6685impl Render for AlterUser {
6686    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6687        f.write_str("ALTER USER ")?;
6688        match self {
6689            Self::Modify {
6690                if_exists,
6691                users,
6692                require,
6693                resource_options,
6694                password_lock_options,
6695                attribute,
6696                ..
6697            } => {
6698                if *if_exists {
6699                    f.write_str("IF EXISTS ")?;
6700                }
6701                render_comma_separated(users, ctx, f)?;
6702                render_user_option_tail(
6703                    UserOptionTail {
6704                        require,
6705                        resource_options,
6706                        password_lock_options,
6707                        attribute,
6708                    },
6709                    ctx,
6710                    f,
6711                )
6712            }
6713            Self::DefaultRole {
6714                if_exists,
6715                user,
6716                roles,
6717                ..
6718            } => {
6719                if *if_exists {
6720                    f.write_str("IF EXISTS ")?;
6721                }
6722                user.render(ctx, f)?;
6723                f.write_str(" DEFAULT ROLE ")?;
6724                roles.render(ctx, f)
6725            }
6726        }
6727    }
6728}
6729
6730impl Render for AlterUserSpec {
6731    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6732        self.account.render(ctx, f)?;
6733        if let Some(auth) = &self.auth {
6734            f.write_str(" ")?;
6735            auth.render(ctx, f)?;
6736        }
6737        if let Some(replace) = &self.replace {
6738            f.write_str(" REPLACE ")?;
6739            replace.render(ctx, f)?;
6740        }
6741        if self.retain_current_password {
6742            f.write_str(" RETAIN CURRENT PASSWORD")?;
6743        }
6744        if self.discard_old_password {
6745            f.write_str(" DISCARD OLD PASSWORD")?;
6746        }
6747        Ok(())
6748    }
6749}
6750
6751impl Render for DefaultRoleTarget {
6752    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6753        match self {
6754            Self::All { .. } => f.write_str("ALL"),
6755            Self::None { .. } => f.write_str("NONE"),
6756            Self::Roles { roles, .. } => render_comma_separated(roles, ctx, f),
6757        }
6758    }
6759}
6760
6761impl Render for UserRoleList {
6762    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6763        let (verb, guard) = match self.kind {
6764            UserRoleListKind::DropUser => ("DROP USER ", "IF EXISTS "),
6765            UserRoleListKind::CreateRole => ("CREATE ROLE ", "IF NOT EXISTS "),
6766            UserRoleListKind::DropRole => ("DROP ROLE ", "IF EXISTS "),
6767        };
6768        f.write_str(verb)?;
6769        if self.if_guard {
6770            f.write_str(guard)?;
6771        }
6772        render_comma_separated(&self.names, ctx, f)
6773    }
6774}
6775
6776/// The surface keyword for a [`NoWriteToBinlog`] binlog-suppression prefix.
6777fn no_write_to_binlog_keyword(prefix: NoWriteToBinlog) -> &'static str {
6778    match prefix {
6779        NoWriteToBinlog::NoWriteToBinlog => "NO_WRITE_TO_BINLOG",
6780        NoWriteToBinlog::Local => "LOCAL",
6781    }
6782}
6783
6784/// The surface keyword for a [`TableKeyword`] (`TABLE`/`TABLES`).
6785fn table_keyword_str(keyword: TableKeyword) -> &'static str {
6786    match keyword {
6787        TableKeyword::Table => "TABLE",
6788        TableKeyword::Tables => "TABLES",
6789    }
6790}
6791
6792/// The surface keyword(s) for a [`CheckTableOption`].
6793fn check_table_option_keyword(option: CheckTableOption) -> &'static str {
6794    match option {
6795        CheckTableOption::ForUpgrade => "FOR UPGRADE",
6796        CheckTableOption::Quick => "QUICK",
6797        CheckTableOption::Fast => "FAST",
6798        CheckTableOption::Medium => "MEDIUM",
6799        CheckTableOption::Extended => "EXTENDED",
6800        CheckTableOption::Changed => "CHANGED",
6801    }
6802}
6803
6804/// The surface keyword for a [`ChecksumTableOption`].
6805fn checksum_table_option_keyword(option: ChecksumTableOption) -> &'static str {
6806    match option {
6807        ChecksumTableOption::Quick => "QUICK",
6808        ChecksumTableOption::Extended => "EXTENDED",
6809    }
6810}
6811
6812/// The surface keyword for a [`RepairTableOption`].
6813fn repair_table_option_keyword(option: RepairTableOption) -> &'static str {
6814    match option {
6815        RepairTableOption::Quick => "QUICK",
6816        RepairTableOption::Extended => "EXTENDED",
6817        RepairTableOption::UseFrm => "USE_FRM",
6818    }
6819}
6820
6821impl<X: Extension + Render> Render for PrepareStatement<X> {
6822    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6823        f.write_str("PREPARE ")?;
6824        self.name.render(ctx, f)?;
6825        // An empty `parameter_types` is the bare `PREPARE name AS ...` form (no list);
6826        // PostgreSQL rejects an empty written `()`, so a non-empty list unambiguously
6827        // means the parentheses were written.
6828        if !self.parameter_types.is_empty() {
6829            f.write_str("(")?;
6830            render_comma_separated(&self.parameter_types, ctx, f)?;
6831            f.write_str(")")?;
6832        }
6833        f.write_str(" AS ")?;
6834        self.statement.render(ctx, f)
6835    }
6836}
6837
6838impl<X: Extension + Render> Render for ExecuteStatement<X> {
6839    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6840        f.write_str("EXECUTE ")?;
6841        self.name.render(ctx, f)?;
6842        // An empty `args` is the bare `EXECUTE v1` form (no list); a non-empty one renders
6843        // the parenthesized arguments. An empty written `()` is never built (parser-rejected).
6844        if !self.args.is_empty() {
6845            f.write_str("(")?;
6846            render_comma_separated(&self.args, ctx, f)?;
6847            f.write_str(")")?;
6848        }
6849        Ok(())
6850    }
6851}
6852
6853impl Render for DeallocateStatement {
6854    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6855        // The leading verb round-trips MySQL's `deallocate_or_drop` synonym choice; DuckDB
6856        // only ever spells `DEALLOCATE`.
6857        f.write_str(match self.keyword {
6858            DeallocateKeyword::Deallocate => "DEALLOCATE ",
6859            DeallocateKeyword::Drop => "DROP ",
6860        })?;
6861        if self.prepare_keyword {
6862            f.write_str("PREPARE ")?;
6863        }
6864        self.name.render(ctx, f)
6865    }
6866}
6867
6868impl Render for PrepareFromStatement {
6869    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6870        f.write_str("PREPARE ")?;
6871        self.name.render(ctx, f)?;
6872        f.write_str(" FROM ")?;
6873        self.source.render(ctx, f)
6874    }
6875}
6876
6877impl Render for PrepareSource {
6878    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6879        match self {
6880            // The string source round-trips verbatim from the `Literal`.
6881            PrepareSource::Text { source, .. } => source.render(ctx, f),
6882            // A `@variable` source: the `@` sigil plus the name, whose quote style the `Ident`
6883            // carries.
6884            PrepareSource::Variable { name, .. } => {
6885                f.write_str("@")?;
6886                name.render(ctx, f)
6887            }
6888        }
6889    }
6890}
6891
6892impl Render for ExecuteUsingStatement {
6893    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6894        f.write_str("EXECUTE ")?;
6895        self.name.render(ctx, f)?;
6896        // A non-empty list renders the `USING` clause; an empty one is the bare `EXECUTE name`
6897        // form (no `USING` written — MySQL has no empty-`USING` spelling).
6898        if let Some((first, rest)) = self.using.split_first() {
6899            f.write_str(" USING @")?;
6900            first.render(ctx, f)?;
6901            for name in rest {
6902                f.write_str(", @")?;
6903                name.render(ctx, f)?;
6904            }
6905        }
6906        Ok(())
6907    }
6908}
6909
6910impl<X: Extension + Render> Render for CallStatement<X> {
6911    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6912        f.write_str("CALL ")?;
6913        self.name.render(ctx, f)?;
6914        // The parenthesized argument list is a surface flag: the DuckDB form always writes
6915        // it (empty or not), MySQL's bare `CALL name` writes no list at all.
6916        if self.parenthesized {
6917            f.write_str("(")?;
6918            render_comma_separated(&self.args, ctx, f)?;
6919            f.write_str(")")?;
6920        }
6921        Ok(())
6922    }
6923}
6924
6925impl Render for DoStatement {
6926    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6927        f.write_str("DO")?;
6928        // The list is always non-empty (the parser rejects a bare `DO`); each item follows a
6929        // single space, and the source order round-trips (a body and a language clause can
6930        // appear in either order, and either may repeat).
6931        for arg in &self.args {
6932            f.write_str(" ")?;
6933            arg.render(ctx, f)?;
6934        }
6935        Ok(())
6936    }
6937}
6938
6939impl Render for DoArg {
6940    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6941        match self {
6942            DoArg::Body { body, .. } => body.render(ctx, f),
6943            DoArg::Language { name, .. } => {
6944                f.write_str("LANGUAGE ")?;
6945                name.render(ctx, f)
6946            }
6947        }
6948    }
6949}
6950
6951impl Render for LanguageName {
6952    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6953        match self {
6954            Self::Word { word, .. } => word.render(ctx, f),
6955            Self::String { value, .. } => value.render(ctx, f),
6956        }
6957    }
6958}
6959
6960impl<X: Extension + Render> Render for DoExpressionsStatement<X> {
6961    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6962        // The list is always non-empty (the parser rejects a bare `DO`).
6963        f.write_str("DO ")?;
6964        render_comma_separated(&self.items, ctx, f)
6965    }
6966}
6967
6968impl Render for LockTablesStatement {
6969    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6970        // `plural` preserves the interchangeable `TABLES`/`TABLE` source spelling; the
6971        // list is always non-empty (the parser rejects a bare `LOCK TABLES`).
6972        f.write_str(if self.plural {
6973            "LOCK TABLES "
6974        } else {
6975            "LOCK TABLE "
6976        })?;
6977        render_comma_separated(&self.tables, ctx, f)
6978    }
6979}
6980
6981impl Render for TableLock {
6982    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6983        self.name.render(ctx, f)?;
6984        if let Some(alias) = &self.alias {
6985            // The canonical `AS`-less spelling: MySQL's `opt_as` makes the keyword pure
6986            // noise the AST does not record.
6987            f.write_str(" ")?;
6988            alias.render(ctx, f)?;
6989        }
6990        f.write_str(" ")?;
6991        self.kind.render(ctx, f)
6992    }
6993}
6994
6995impl Render for TableLockKind {
6996    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6997        f.write_str(match self {
6998            TableLockKind::Read => "READ",
6999            TableLockKind::ReadLocal => "READ LOCAL",
7000            TableLockKind::Write => "WRITE",
7001        })
7002    }
7003}
7004
7005impl Render for UnlockTablesStatement {
7006    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7007        f.write_str(if self.plural {
7008            "UNLOCK TABLES"
7009        } else {
7010            "UNLOCK TABLE"
7011        })
7012    }
7013}
7014
7015impl Render for InstanceLockStatement {
7016    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7017        f.write_str(if self.acquire {
7018            "LOCK INSTANCE FOR BACKUP"
7019        } else {
7020            "UNLOCK INSTANCE"
7021        })
7022    }
7023}
7024
7025impl<X: Extension + Render> Render for LoadDataStatement<X> {
7026    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7027        // The grammar's canonical clause order — the parse layer is order-sensitive, so the
7028        // rendered order is the only round-trippable one.
7029        f.write_str("LOAD ")?;
7030        self.format.render(ctx, f)?;
7031        if let Some(concurrency) = self.concurrency {
7032            f.write_str(" ")?;
7033            concurrency.render(ctx, f)?;
7034        }
7035        if self.local {
7036            f.write_str(" LOCAL")?;
7037        }
7038        f.write_str(" INFILE ")?;
7039        self.file.render(ctx, f)?;
7040        if let Some(on_duplicate) = self.on_duplicate {
7041            f.write_str(" ")?;
7042            on_duplicate.render(ctx, f)?;
7043        }
7044        f.write_str(" INTO TABLE ")?;
7045        self.table.render(ctx, f)?;
7046        if !self.partitions.is_empty() {
7047            f.write_str(" PARTITION (")?;
7048            render_comma_separated(&self.partitions, ctx, f)?;
7049            f.write_str(")")?;
7050        }
7051        if let Some(charset) = &self.charset {
7052            f.write_str(" CHARACTER SET ")?;
7053            charset.render(ctx, f)?;
7054        }
7055        if let Some(tag) = &self.rows_identified_by {
7056            f.write_str(" ROWS IDENTIFIED BY ")?;
7057            tag.render(ctx, f)?;
7058        }
7059        if let Some(fields) = &self.fields {
7060            f.write_str(" ")?;
7061            fields.render(ctx, f)?;
7062        }
7063        if let Some(lines) = &self.lines {
7064            f.write_str(" ")?;
7065            lines.render(ctx, f)?;
7066        }
7067        if let Some(ignore_rows) = &self.ignore_rows {
7068            f.write_str(" ")?;
7069            ignore_rows.render(ctx, f)?;
7070        }
7071        if !self.columns.is_empty() {
7072            f.write_str(" (")?;
7073            render_comma_separated(&self.columns, ctx, f)?;
7074            f.write_str(")")?;
7075        }
7076        if !self.set.is_empty() {
7077            f.write_str(" SET ")?;
7078            render_comma_separated(&self.set, ctx, f)?;
7079        }
7080        Ok(())
7081    }
7082}
7083
7084impl Render for LoadDataFormat {
7085    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7086        f.write_str(match self {
7087            LoadDataFormat::Data => "DATA",
7088            LoadDataFormat::Xml => "XML",
7089        })
7090    }
7091}
7092
7093impl Render for LoadDataConcurrency {
7094    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7095        f.write_str(match self {
7096            LoadDataConcurrency::LowPriority => "LOW_PRIORITY",
7097            LoadDataConcurrency::Concurrent => "CONCURRENT",
7098        })
7099    }
7100}
7101
7102impl Render for LoadDataDuplicate {
7103    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7104        f.write_str(match self {
7105            LoadDataDuplicate::Replace => "REPLACE",
7106            LoadDataDuplicate::Ignore => "IGNORE",
7107        })
7108    }
7109}
7110
7111impl Render for LoadDataFields {
7112    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7113        // The sub-clauses render in the grammar's canonical order regardless of source order
7114        // (any order re-parses to the same node); at least one is always present.
7115        self.spelling.render(ctx, f)?;
7116        if let Some(terminated_by) = &self.terminated_by {
7117            f.write_str(" TERMINATED BY ")?;
7118            terminated_by.render(ctx, f)?;
7119        }
7120        if let Some(enclosed_by) = &self.enclosed_by {
7121            f.write_str(" ")?;
7122            enclosed_by.render(ctx, f)?;
7123        }
7124        if let Some(escaped_by) = &self.escaped_by {
7125            f.write_str(" ESCAPED BY ")?;
7126            escaped_by.render(ctx, f)?;
7127        }
7128        Ok(())
7129    }
7130}
7131
7132impl Render for LoadFieldsSpelling {
7133    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7134        f.write_str(match self {
7135            LoadFieldsSpelling::Fields => "FIELDS",
7136            LoadFieldsSpelling::Columns => "COLUMNS",
7137        })
7138    }
7139}
7140
7141impl Render for LoadDataEnclosed {
7142    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7143        if self.optionally {
7144            f.write_str("OPTIONALLY ")?;
7145        }
7146        f.write_str("ENCLOSED BY ")?;
7147        self.value.render(ctx, f)
7148    }
7149}
7150
7151impl Render for LoadDataLines {
7152    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7153        f.write_str("LINES")?;
7154        if let Some(starting_by) = &self.starting_by {
7155            f.write_str(" STARTING BY ")?;
7156            starting_by.render(ctx, f)?;
7157        }
7158        if let Some(terminated_by) = &self.terminated_by {
7159            f.write_str(" TERMINATED BY ")?;
7160            terminated_by.render(ctx, f)?;
7161        }
7162        Ok(())
7163    }
7164}
7165
7166impl Render for LoadDataIgnoreRows {
7167    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7168        f.write_str("IGNORE ")?;
7169        self.count.render(ctx, f)?;
7170        f.write_str(" ")?;
7171        self.unit.render(ctx, f)
7172    }
7173}
7174
7175impl Render for LoadDataIgnoreUnit {
7176    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7177        f.write_str(match self {
7178            LoadDataIgnoreUnit::Lines => "LINES",
7179            LoadDataIgnoreUnit::Rows => "ROWS",
7180        })
7181    }
7182}
7183
7184impl Render for LoadDataFieldOrVar {
7185    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7186        match self {
7187            LoadDataFieldOrVar::Column { name, .. } => name.render(ctx, f),
7188            // A `@variable` target: the `@` sigil plus the name (whose quote style the `Ident`
7189            // carries).
7190            LoadDataFieldOrVar::Variable { name, .. } => {
7191                f.write_str("@")?;
7192                name.render(ctx, f)
7193            }
7194        }
7195    }
7196}
7197
7198impl<X: Extension + Render> Render for CreateTrigger<X> {
7199    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7200        f.write_str("CREATE")?;
7201        if let Some(temporary) = self.temporary {
7202            f.write_str(" ")?;
7203            temporary.render(ctx, f)?;
7204        }
7205        f.write_str(" TRIGGER")?;
7206        if self.if_not_exists {
7207            f.write_str(" IF NOT EXISTS")?;
7208        }
7209        f.write_str(" ")?;
7210        self.name.render(ctx, f)?;
7211        if let Some(timing) = self.timing {
7212            f.write_str(" ")?;
7213            timing.render(ctx, f)?;
7214        }
7215        f.write_str(" ")?;
7216        self.event.render(ctx, f)?;
7217        f.write_str(" ON ")?;
7218        self.table.render(ctx, f)?;
7219        if self.for_each_row {
7220            f.write_str(" FOR EACH ROW")?;
7221        }
7222        if let Some(when) = &self.when {
7223            f.write_str(" WHEN ")?;
7224            when.render(ctx, f)?;
7225        }
7226        f.write_str(" BEGIN ")?;
7227        for statement in &self.body {
7228            statement.render(ctx, f)?;
7229            f.write_str("; ")?;
7230        }
7231        f.write_str("END")
7232    }
7233}
7234
7235impl Render for TriggerTiming {
7236    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7237        f.write_str(match self {
7238            TriggerTiming::Before => "BEFORE",
7239            TriggerTiming::After => "AFTER",
7240            TriggerTiming::InsteadOf => "INSTEAD OF",
7241        })
7242    }
7243}
7244
7245impl Render for TriggerEvent {
7246    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7247        match self {
7248            TriggerEvent::Delete { .. } => f.write_str("DELETE"),
7249            TriggerEvent::Insert { .. } => f.write_str("INSERT"),
7250            TriggerEvent::Update { columns, .. } => {
7251                f.write_str("UPDATE")?;
7252                if !columns.is_empty() {
7253                    f.write_str(" OF ")?;
7254                    render_ident_list(columns, ctx, f)?;
7255                }
7256                Ok(())
7257            }
7258        }
7259    }
7260}
7261
7262impl<X: Extension + Render> Render for CreateStoredTrigger<X> {
7263    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7264        f.write_str("CREATE ")?;
7265        if let Some(definer) = &self.definer {
7266            definer.render(ctx, f)?;
7267            f.write_str(" ")?;
7268        }
7269        f.write_str("TRIGGER ")?;
7270        if self.if_not_exists {
7271            f.write_str("IF NOT EXISTS ")?;
7272        }
7273        self.name.render(ctx, f)?;
7274        f.write_str(" ")?;
7275        self.timing.render(ctx, f)?;
7276        f.write_str(" ")?;
7277        self.event.render(ctx, f)?;
7278        f.write_str(" ON ")?;
7279        self.table.render(ctx, f)?;
7280        f.write_str(" FOR EACH ROW")?;
7281        if let Some(ordering) = &self.ordering {
7282            f.write_str(" ")?;
7283            ordering.render(ctx, f)?;
7284        }
7285        f.write_str(" ")?;
7286        self.body.render(ctx, f)
7287    }
7288}
7289
7290impl Render for TriggerOrder {
7291    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7292        match self {
7293            TriggerOrder::Follows { anchor, .. } => {
7294                f.write_str("FOLLOWS ")?;
7295                anchor.render(ctx, f)
7296            }
7297            TriggerOrder::Precedes { anchor, .. } => {
7298                f.write_str("PRECEDES ")?;
7299                anchor.render(ctx, f)
7300            }
7301        }
7302    }
7303}
7304
7305// ---------------------------------------------------------------------------
7306// Stored-program compound statements (MySQL SQL/PSM)
7307// ---------------------------------------------------------------------------
7308
7309/// Render a `;`-terminated compound-body statement list, each element prefixed by a
7310/// single leading space (` <stmt>;`) — the shared shape of every block, branch, and
7311/// loop body, mirroring the trigger body's `<stmt>; ` join.
7312fn render_compound_body<X: Extension + Render>(
7313    body: &[Statement<X>],
7314    ctx: &RenderCtx<'_>,
7315    f: &mut fmt::Formatter<'_>,
7316) -> fmt::Result {
7317    for statement in body {
7318        f.write_str(" ")?;
7319        statement.render(ctx, f)?;
7320        f.write_str(";")?;
7321    }
7322    Ok(())
7323}
7324
7325impl<X: Extension + Render> Render for CompoundStatement<X> {
7326    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7327        if let Some(label) = &self.label {
7328            label.render(ctx, f)?;
7329            f.write_str(": ")?;
7330        }
7331        f.write_str("BEGIN")?;
7332        for declaration in &self.declarations {
7333            f.write_str(" ")?;
7334            declaration.render(ctx, f)?;
7335            f.write_str(";")?;
7336        }
7337        render_compound_body(&self.body, ctx, f)?;
7338        f.write_str(" END")?;
7339        if let Some(end_label) = &self.end_label {
7340            f.write_str(" ")?;
7341            end_label.render(ctx, f)?;
7342        }
7343        Ok(())
7344    }
7345}
7346
7347impl<X: Extension + Render> Render for Declaration<X> {
7348    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7349        match self {
7350            Declaration::Variable {
7351                names,
7352                data_type,
7353                default,
7354                ..
7355            } => {
7356                f.write_str("DECLARE ")?;
7357                render_ident_list(names, ctx, f)?;
7358                f.write_str(" ")?;
7359                data_type.render(ctx, f)?;
7360                if let Some(default) = default {
7361                    f.write_str(" DEFAULT ")?;
7362                    default.render(ctx, f)?;
7363                }
7364                Ok(())
7365            }
7366            Declaration::Condition { name, value, .. } => {
7367                f.write_str("DECLARE ")?;
7368                name.render(ctx, f)?;
7369                f.write_str(" CONDITION FOR ")?;
7370                value.render(ctx, f)
7371            }
7372            Declaration::Cursor { name, query, .. } => {
7373                f.write_str("DECLARE ")?;
7374                name.render(ctx, f)?;
7375                f.write_str(" CURSOR FOR ")?;
7376                query.render(ctx, f)
7377            }
7378            Declaration::Handler {
7379                action,
7380                conditions,
7381                body,
7382                ..
7383            } => {
7384                f.write_str("DECLARE ")?;
7385                action.render(ctx, f)?;
7386                f.write_str(" HANDLER FOR ")?;
7387                render_comma_separated(conditions, ctx, f)?;
7388                f.write_str(" ")?;
7389                body.render(ctx, f)
7390            }
7391        }
7392    }
7393}
7394
7395impl Render for HandlerAction {
7396    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7397        f.write_str(match self {
7398            HandlerAction::Continue => "CONTINUE",
7399            HandlerAction::Exit => "EXIT",
7400            HandlerAction::Undo => "UNDO",
7401        })
7402    }
7403}
7404
7405impl Render for ConditionValue {
7406    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7407        match self {
7408            ConditionValue::SqlState {
7409                value_keyword,
7410                sqlstate,
7411                ..
7412            } => {
7413                f.write_str("SQLSTATE ")?;
7414                if *value_keyword {
7415                    f.write_str("VALUE ")?;
7416                }
7417                sqlstate.render(ctx, f)
7418            }
7419            ConditionValue::ErrorCode { code, .. } => code.render(ctx, f),
7420            ConditionValue::ConditionName { name, .. } => name.render(ctx, f),
7421        }
7422    }
7423}
7424
7425impl Render for HandlerCondition {
7426    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7427        match self {
7428            HandlerCondition::SqlState {
7429                value_keyword,
7430                sqlstate,
7431                ..
7432            } => {
7433                f.write_str("SQLSTATE ")?;
7434                if *value_keyword {
7435                    f.write_str("VALUE ")?;
7436                }
7437                sqlstate.render(ctx, f)
7438            }
7439            HandlerCondition::ErrorCode { code, .. } => code.render(ctx, f),
7440            HandlerCondition::ConditionName { name, .. } => name.render(ctx, f),
7441            HandlerCondition::SqlWarning { .. } => f.write_str("SQLWARNING"),
7442            HandlerCondition::NotFound { .. } => f.write_str("NOT FOUND"),
7443            HandlerCondition::SqlException { .. } => f.write_str("SQLEXCEPTION"),
7444        }
7445    }
7446}
7447
7448impl<X: Extension + Render> Render for ConditionalBranch<X> {
7449    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7450        self.guard.render(ctx, f)?;
7451        f.write_str(" THEN")?;
7452        render_compound_body(&self.body, ctx, f)
7453    }
7454}
7455
7456impl<X: Extension + Render> Render for IfStatement<X> {
7457    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7458        f.write_str("IF ")?;
7459        for (i, branch) in self.branches.iter().enumerate() {
7460            if i > 0 {
7461                f.write_str(" ELSEIF ")?;
7462            }
7463            branch.render(ctx, f)?;
7464        }
7465        if let Some(else_body) = &self.else_body {
7466            f.write_str(" ELSE")?;
7467            render_compound_body(else_body, ctx, f)?;
7468        }
7469        f.write_str(" END IF")
7470    }
7471}
7472
7473impl<X: Extension + Render> Render for CaseStatement<X> {
7474    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7475        f.write_str("CASE")?;
7476        if let Some(operand) = &self.operand {
7477            f.write_str(" ")?;
7478            operand.render(ctx, f)?;
7479        }
7480        for branch in &self.when_branches {
7481            f.write_str(" WHEN ")?;
7482            branch.render(ctx, f)?;
7483        }
7484        if let Some(else_body) = &self.else_body {
7485            f.write_str(" ELSE")?;
7486            render_compound_body(else_body, ctx, f)?;
7487        }
7488        f.write_str(" END CASE")
7489    }
7490}
7491
7492impl<X: Extension + Render> Render for LoopStatement<X> {
7493    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7494        if let Some(label) = &self.label {
7495            label.render(ctx, f)?;
7496            f.write_str(": ")?;
7497        }
7498        f.write_str("LOOP")?;
7499        render_compound_body(&self.body, ctx, f)?;
7500        f.write_str(" END LOOP")?;
7501        if let Some(end_label) = &self.end_label {
7502            f.write_str(" ")?;
7503            end_label.render(ctx, f)?;
7504        }
7505        Ok(())
7506    }
7507}
7508
7509impl<X: Extension + Render> Render for WhileStatement<X> {
7510    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7511        if let Some(label) = &self.label {
7512            label.render(ctx, f)?;
7513            f.write_str(": ")?;
7514        }
7515        f.write_str("WHILE ")?;
7516        self.condition.render(ctx, f)?;
7517        f.write_str(" DO")?;
7518        render_compound_body(&self.body, ctx, f)?;
7519        f.write_str(" END WHILE")?;
7520        if let Some(end_label) = &self.end_label {
7521            f.write_str(" ")?;
7522            end_label.render(ctx, f)?;
7523        }
7524        Ok(())
7525    }
7526}
7527
7528impl<X: Extension + Render> Render for RepeatStatement<X> {
7529    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7530        if let Some(label) = &self.label {
7531            label.render(ctx, f)?;
7532            f.write_str(": ")?;
7533        }
7534        f.write_str("REPEAT")?;
7535        render_compound_body(&self.body, ctx, f)?;
7536        f.write_str(" UNTIL ")?;
7537        self.condition.render(ctx, f)?;
7538        f.write_str(" END REPEAT")?;
7539        if let Some(end_label) = &self.end_label {
7540            f.write_str(" ")?;
7541            end_label.render(ctx, f)?;
7542        }
7543        Ok(())
7544    }
7545}
7546
7547impl Render for LeaveStatement {
7548    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7549        f.write_str("LEAVE ")?;
7550        self.label.render(ctx, f)
7551    }
7552}
7553
7554impl Render for IterateStatement {
7555    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7556        f.write_str("ITERATE ")?;
7557        self.label.render(ctx, f)
7558    }
7559}
7560
7561impl<X: Extension + Render> Render for ReturnStatement<X> {
7562    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7563        f.write_str("RETURN ")?;
7564        self.value.render(ctx, f)
7565    }
7566}
7567
7568impl Render for OpenCursorStatement {
7569    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7570        f.write_str("OPEN ")?;
7571        self.cursor.render(ctx, f)
7572    }
7573}
7574
7575impl Render for CloseCursorStatement {
7576    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7577        f.write_str("CLOSE ")?;
7578        self.cursor.render(ctx, f)
7579    }
7580}
7581
7582impl Render for FetchCursorStatement {
7583    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7584        f.write_str("FETCH ")?;
7585        if self.next_keyword {
7586            f.write_str("NEXT ")?;
7587        }
7588        if self.from_keyword {
7589            f.write_str("FROM ")?;
7590        }
7591        self.cursor.render(ctx, f)?;
7592        f.write_str(" INTO ")?;
7593        render_ident_list(&self.targets, ctx, f)
7594    }
7595}
7596
7597impl<X: Extension + Render> SignalStatement<X> {
7598    /// Render as `SIGNAL`/`RESIGNAL` — the two share this payload and differ only in the
7599    /// leading keyword the caller supplies.
7600    fn render_as(
7601        &self,
7602        ctx: &RenderCtx<'_>,
7603        f: &mut fmt::Formatter<'_>,
7604        keyword: &str,
7605    ) -> fmt::Result {
7606        f.write_str(keyword)?;
7607        if let Some(condition) = &self.condition {
7608            f.write_str(" ")?;
7609            condition.render(ctx, f)?;
7610        }
7611        if !self.set_items.is_empty() {
7612            f.write_str(" SET ")?;
7613            render_comma_separated(&self.set_items, ctx, f)?;
7614        }
7615        Ok(())
7616    }
7617}
7618
7619impl<X: Extension + Render> Render for SignalItem<X> {
7620    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7621        self.name.render(ctx, f)?;
7622        f.write_str(" = ")?;
7623        self.value.render(ctx, f)
7624    }
7625}
7626
7627impl Render for SignalItemName {
7628    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7629        f.write_str(match self {
7630            SignalItemName::ClassOrigin => "CLASS_ORIGIN",
7631            SignalItemName::SubclassOrigin => "SUBCLASS_ORIGIN",
7632            SignalItemName::ConstraintCatalog => "CONSTRAINT_CATALOG",
7633            SignalItemName::ConstraintSchema => "CONSTRAINT_SCHEMA",
7634            SignalItemName::ConstraintName => "CONSTRAINT_NAME",
7635            SignalItemName::CatalogName => "CATALOG_NAME",
7636            SignalItemName::SchemaName => "SCHEMA_NAME",
7637            SignalItemName::TableName => "TABLE_NAME",
7638            SignalItemName::ColumnName => "COLUMN_NAME",
7639            SignalItemName::CursorName => "CURSOR_NAME",
7640            SignalItemName::MessageText => "MESSAGE_TEXT",
7641            SignalItemName::MysqlErrno => "MYSQL_ERRNO",
7642        })
7643    }
7644}
7645
7646impl<X: Extension + Render> Render for GetDiagnosticsStatement<X> {
7647    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7648        f.write_str("GET")?;
7649        match self.area {
7650            DiagnosticsArea::Implicit => {}
7651            DiagnosticsArea::Current => f.write_str(" CURRENT")?,
7652            DiagnosticsArea::Stacked => f.write_str(" STACKED")?,
7653        }
7654        f.write_str(" DIAGNOSTICS ")?;
7655        self.info.render(ctx, f)
7656    }
7657}
7658
7659impl<X: Extension + Render> Render for DiagnosticsInfo<X> {
7660    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7661        match self {
7662            DiagnosticsInfo::Statement { items, .. } => render_comma_separated(items, ctx, f),
7663            DiagnosticsInfo::Condition { number, items, .. } => {
7664                f.write_str("CONDITION ")?;
7665                number.render(ctx, f)?;
7666                f.write_str(" ")?;
7667                render_comma_separated(items, ctx, f)
7668            }
7669        }
7670    }
7671}
7672
7673impl<X: Extension + Render> Render for StatementInfoItem<X> {
7674    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7675        self.target.render(ctx, f)?;
7676        f.write_str(" = ")?;
7677        self.name.render(ctx, f)
7678    }
7679}
7680
7681impl Render for StatementInfoItemName {
7682    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7683        f.write_str(match self {
7684            StatementInfoItemName::Number => "NUMBER",
7685            StatementInfoItemName::RowCount => "ROW_COUNT",
7686        })
7687    }
7688}
7689
7690impl<X: Extension + Render> Render for ConditionInfoItem<X> {
7691    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7692        self.target.render(ctx, f)?;
7693        f.write_str(" = ")?;
7694        self.name.render(ctx, f)
7695    }
7696}
7697
7698impl Render for ConditionInfoItemName {
7699    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7700        f.write_str(match self {
7701            ConditionInfoItemName::ClassOrigin => "CLASS_ORIGIN",
7702            ConditionInfoItemName::SubclassOrigin => "SUBCLASS_ORIGIN",
7703            ConditionInfoItemName::ConstraintCatalog => "CONSTRAINT_CATALOG",
7704            ConditionInfoItemName::ConstraintSchema => "CONSTRAINT_SCHEMA",
7705            ConditionInfoItemName::ConstraintName => "CONSTRAINT_NAME",
7706            ConditionInfoItemName::CatalogName => "CATALOG_NAME",
7707            ConditionInfoItemName::SchemaName => "SCHEMA_NAME",
7708            ConditionInfoItemName::TableName => "TABLE_NAME",
7709            ConditionInfoItemName::ColumnName => "COLUMN_NAME",
7710            ConditionInfoItemName::CursorName => "CURSOR_NAME",
7711            ConditionInfoItemName::MessageText => "MESSAGE_TEXT",
7712            ConditionInfoItemName::MysqlErrno => "MYSQL_ERRNO",
7713            ConditionInfoItemName::ReturnedSqlstate => "RETURNED_SQLSTATE",
7714        })
7715    }
7716}
7717
7718impl<X: Extension + Render> Render for CreateTable<X> {
7719    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7720        f.write_str("CREATE")?;
7721        if self.or_replace {
7722            f.write_str(" OR REPLACE")?;
7723        }
7724        if let Some(temporary) = self.temporary {
7725            f.write_str(" ")?;
7726            temporary.render(ctx, f)?;
7727        }
7728        // `UNLOGGED` is a peer of `TEMP`/`TEMPORARY` in PostgreSQL's `OptTemp`, so it never
7729        // co-occurs with `temporary` — exactly one persistence keyword is written.
7730        if self.unlogged {
7731            f.write_str(" UNLOGGED")?;
7732        }
7733        f.write_str(" TABLE")?;
7734        if self.if_not_exists {
7735            f.write_str(" IF NOT EXISTS")?;
7736        }
7737        f.write_str(" ")?;
7738        self.name.render(ctx, f)?;
7739        match &self.body {
7740            // PostgreSQL grammar order after the body: `INHERITS (…)`, then `PARTITION BY …`,
7741            // then `USING <method>`, then the trailing options. `inherits` is only ever non-empty
7742            // on a `Definition` body (never `PartitionOf`/`OfType`), and an `AS <query>` body
7743            // carries none of these, so they are unrendered there.
7744            CreateTableBody::Definition { .. }
7745            | CreateTableBody::PartitionOf { .. }
7746            | CreateTableBody::OfType { .. }
7747            // MySQL's `LIKE <source>` clone body carries no trailing clause of its own (the
7748            // grammar rejects options after it), so `inherits`/`partition_by`/`access_method`/
7749            // `options` are empty/`None` and their renders are no-ops here.
7750            | CreateTableBody::LikeSource { .. } => {
7751                self.body.render(ctx, f)?;
7752                render_inherits(&self.inherits, ctx, f)?;
7753                render_partition_by(&self.partition_by, ctx, f)?;
7754                render_access_method(&self.access_method, ctx, f)?;
7755                render_create_table_options(&self.options, ctx, f)?;
7756            }
7757            CreateTableBody::AsQuery {
7758                columns,
7759                query,
7760                with_data,
7761                ..
7762            } => {
7763                render_ctas_columns(columns, ctx, f)?;
7764                // The CTAS `USING` slot precedes the options and the `AS` (PostgreSQL's
7765                // `CreateTableAsStmt` order, mirroring the tail order above).
7766                render_access_method(&self.access_method, ctx, f)?;
7767                render_create_table_options(&self.options, ctx, f)?;
7768                render_ctas_query_tail(query, *with_data, ctx, f)?;
7769            }
7770            CreateTableBody::AsExecute {
7771                columns,
7772                execute,
7773                with_data,
7774                ..
7775            } => {
7776                render_ctas_columns(columns, ctx, f)?;
7777                render_access_method(&self.access_method, ctx, f)?;
7778                render_create_table_options(&self.options, ctx, f)?;
7779                f.write_str(" AS ")?;
7780                execute.render(ctx, f)?;
7781                render_with_data(*with_data, f)?;
7782            }
7783        }
7784        Ok(())
7785    }
7786}
7787
7788/// Render the ` INHERITS (<parent>, ...)` clause when the parent list is non-empty; the
7789/// leading space is part of the clause separator, and an empty list (no clause) renders nothing.
7790fn render_inherits(
7791    inherits: &[ObjectName],
7792    ctx: &RenderCtx<'_>,
7793    f: &mut fmt::Formatter<'_>,
7794) -> fmt::Result {
7795    if !inherits.is_empty() {
7796        f.write_str(" INHERITS (")?;
7797        render_comma_separated(inherits, ctx, f)?;
7798        f.write_str(")")?;
7799    }
7800    Ok(())
7801}
7802
7803/// Render the trailing ` PARTITION BY {LIST | RANGE | HASH} (…)` clause when present; the
7804/// leading space is part of the clause separator.
7805fn render_partition_by<X: Extension + Render>(
7806    partition_by: &Option<Box<PartitionSpec<X>>>,
7807    ctx: &RenderCtx<'_>,
7808    f: &mut fmt::Formatter<'_>,
7809) -> fmt::Result {
7810    if let Some(spec) = partition_by {
7811        f.write_str(" PARTITION BY ")?;
7812        spec.render(ctx, f)?;
7813    }
7814    Ok(())
7815}
7816
7817/// Render the trailing ` USING <access_method>` clause when present; the leading space is part
7818/// of the clause separator.
7819fn render_access_method(
7820    access_method: &Option<Box<Ident>>,
7821    ctx: &RenderCtx<'_>,
7822    f: &mut fmt::Formatter<'_>,
7823) -> fmt::Result {
7824    if let Some(method) = access_method {
7825        f.write_str(" USING ")?;
7826        method.render(ctx, f)?;
7827    }
7828    Ok(())
7829}
7830
7831impl Render for TemporaryTableKind {
7832    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7833        f.write_str(match self {
7834            Self::Temp => "TEMP",
7835            Self::Temporary => "TEMPORARY",
7836        })
7837    }
7838}
7839
7840/// Render a `CREATE TABLE`'s trailing options with the dialect-appropriate separator.
7841/// SQLite comma-separates its options (`STRICT, WITHOUT ROWID`); MySQL/PostgreSQL
7842/// space-separate theirs (`ENGINE = InnoDB AUTO_INCREMENT = 100`). The two never mix in
7843/// one statement, so a SQLite keyword-style option after the first carries the comma;
7844/// everything else keeps the plain space, and the first option always leads with a space.
7845fn render_create_table_options<X: Extension + Render>(
7846    options: &[CreateTableOption<X>],
7847    ctx: &RenderCtx<'_>,
7848    f: &mut fmt::Formatter<'_>,
7849) -> fmt::Result {
7850    for (index, option) in options.iter().enumerate() {
7851        let sqlite_option = matches!(
7852            option.kind,
7853            CreateTableOptionKind::WithoutRowid { .. } | CreateTableOptionKind::Strict { .. }
7854        );
7855        f.write_str(if index > 0 && sqlite_option {
7856            ", "
7857        } else {
7858            " "
7859        })?;
7860        option.render(ctx, f)?;
7861    }
7862    Ok(())
7863}
7864
7865impl<X: Extension + Render> Render for CreateTableBody<X> {
7866    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7867        match self {
7868            Self::Definition { elements, .. } => {
7869                f.write_str(" (")?;
7870                render_comma_separated(elements, ctx, f)?;
7871                f.write_str(")")
7872            }
7873            Self::AsQuery {
7874                columns,
7875                query,
7876                with_data,
7877                ..
7878            } => {
7879                render_ctas_columns(columns, ctx, f)?;
7880                render_ctas_query_tail(query, *with_data, ctx, f)
7881            }
7882            Self::AsExecute {
7883                columns,
7884                execute,
7885                with_data,
7886                ..
7887            } => {
7888                render_ctas_columns(columns, ctx, f)?;
7889                f.write_str(" AS ")?;
7890                execute.render(ctx, f)?;
7891                render_with_data(*with_data, f)
7892            }
7893            Self::PartitionOf {
7894                parent,
7895                elements,
7896                bound,
7897                ..
7898            } => {
7899                f.write_str(" PARTITION OF ")?;
7900                parent.render(ctx, f)?;
7901                // The augmentation list is absent (never empty-with-parens: PostgreSQL rejects
7902                // an empty `()`), so an empty vec renders no parentheses.
7903                if !elements.is_empty() {
7904                    f.write_str(" (")?;
7905                    render_comma_separated(elements, ctx, f)?;
7906                    f.write_str(")")?;
7907                }
7908                f.write_str(" ")?;
7909                bound.render(ctx, f)
7910            }
7911            Self::OfType {
7912                type_name,
7913                elements,
7914                ..
7915            } => {
7916                f.write_str(" OF ")?;
7917                type_name.render(ctx, f)?;
7918                // The augmentation list is absent (never empty-with-parens: PostgreSQL rejects an
7919                // empty `()`), so an empty vec renders no parentheses.
7920                if !elements.is_empty() {
7921                    f.write_str(" (")?;
7922                    render_comma_separated(elements, ctx, f)?;
7923                    f.write_str(")")?;
7924                }
7925                Ok(())
7926            }
7927            Self::LikeSource {
7928                source,
7929                parenthesized,
7930                ..
7931            } => {
7932                // MySQL's two spellings differ only by the parentheses `parenthesized` records.
7933                if *parenthesized {
7934                    f.write_str(" (LIKE ")?;
7935                    source.render(ctx, f)?;
7936                    f.write_str(")")
7937                } else {
7938                    f.write_str(" LIKE ")?;
7939                    source.render(ctx, f)
7940                }
7941            }
7942        }
7943    }
7944}
7945
7946impl<X: Extension + Render> Render for PartitionSpec<X> {
7947    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7948        self.strategy.render(ctx, f)?;
7949        f.write_str(" (")?;
7950        render_comma_separated(&self.columns, ctx, f)?;
7951        f.write_str(")")
7952    }
7953}
7954
7955impl Render for PartitionStrategy {
7956    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7957        f.write_str(match self {
7958            Self::List => "LIST",
7959            Self::Range => "RANGE",
7960            Self::Hash => "HASH",
7961        })
7962    }
7963}
7964
7965impl<X: Extension + Render> Render for PartitionElem<X> {
7966    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7967        // A parenthesized key (`(a + b)`) re-adds the grammar-mandated wrapping parentheses; a
7968        // bare column / function-call key renders unwrapped (its own binding power never needs
7969        // them here). The flag, not the expression kind, drives this so `(a)` and `a` round-trip
7970        // distinctly.
7971        if self.parenthesized {
7972            f.write_str("(")?;
7973            self.expr.render(ctx, f)?;
7974            f.write_str(")")?;
7975        } else {
7976            self.expr.render(ctx, f)?;
7977        }
7978        if let Some(collation) = &self.collation {
7979            f.write_str(" COLLATE ")?;
7980            collation.render(ctx, f)?;
7981        }
7982        if let Some(opclass) = &self.opclass {
7983            f.write_str(" ")?;
7984            opclass.render(ctx, f)?;
7985        }
7986        Ok(())
7987    }
7988}
7989
7990impl<X: Extension + Render> Render for PartitionBound<X> {
7991    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7992        match self {
7993            Self::List { values, .. } => {
7994                f.write_str("FOR VALUES IN (")?;
7995                render_comma_separated(values, ctx, f)?;
7996                f.write_str(")")
7997            }
7998            Self::Range { from, to, .. } => {
7999                f.write_str("FOR VALUES FROM (")?;
8000                render_comma_separated(from, ctx, f)?;
8001                f.write_str(") TO (")?;
8002                render_comma_separated(to, ctx, f)?;
8003                f.write_str(")")
8004            }
8005            Self::Hash {
8006                modulus, remainder, ..
8007            } => {
8008                f.write_str("FOR VALUES WITH (MODULUS ")?;
8009                modulus.render(ctx, f)?;
8010                f.write_str(", REMAINDER ")?;
8011                remainder.render(ctx, f)?;
8012                f.write_str(")")
8013            }
8014            Self::Default { .. } => f.write_str("DEFAULT"),
8015        }
8016    }
8017}
8018
8019fn render_ctas_columns(
8020    columns: &[Ident],
8021    ctx: &RenderCtx<'_>,
8022    f: &mut fmt::Formatter<'_>,
8023) -> fmt::Result {
8024    if !columns.is_empty() {
8025        f.write_str(" (")?;
8026        render_ident_list(columns, ctx, f)?;
8027        f.write_str(")")?;
8028    }
8029    Ok(())
8030}
8031
8032fn render_ctas_query_tail<X: Extension + Render>(
8033    query: &Query<X>,
8034    with_data: Option<bool>,
8035    ctx: &RenderCtx<'_>,
8036    f: &mut fmt::Formatter<'_>,
8037) -> fmt::Result {
8038    f.write_str(" AS ")?;
8039    query.render(ctx, f)?;
8040    render_with_data(with_data, f)
8041}
8042
8043/// Render the trailing ` WITH DATA` / ` WITH NO DATA` populate-option of a CTAS or
8044/// materialized-view body, or nothing when unspecified. The leading space is part
8045/// of the clause separator.
8046fn render_with_data(with_data: Option<bool>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8047    match with_data {
8048        Some(true) => f.write_str(" WITH DATA"),
8049        Some(false) => f.write_str(" WITH NO DATA"),
8050        None => Ok(()),
8051    }
8052}
8053
8054impl<X: Extension + Render> Render for TableElement<X> {
8055    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8056        match self {
8057            Self::Column { column, .. } => column.render(ctx, f),
8058            Self::Constraint { constraint, .. } => constraint.render(ctx, f),
8059            Self::Like {
8060                source, options, ..
8061            } => {
8062                f.write_str("LIKE ")?;
8063                source.render(ctx, f)?;
8064                for option in options {
8065                    f.write_str(" ")?;
8066                    option.render(ctx, f)?;
8067                }
8068                Ok(())
8069            }
8070        }
8071    }
8072}
8073
8074impl Render for TableLikeOption {
8075    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8076        self.action.render(ctx, f)?;
8077        f.write_str(" ")?;
8078        self.feature.render(ctx, f)
8079    }
8080}
8081
8082impl Render for TableLikeAction {
8083    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8084        f.write_str(match self {
8085            Self::Including => "INCLUDING",
8086            Self::Excluding => "EXCLUDING",
8087        })
8088    }
8089}
8090
8091impl Render for TableLikeFeature {
8092    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8093        f.write_str(match self {
8094            Self::Comments => "COMMENTS",
8095            Self::Compression => "COMPRESSION",
8096            Self::Constraints => "CONSTRAINTS",
8097            Self::Defaults => "DEFAULTS",
8098            Self::Generated => "GENERATED",
8099            Self::Identity => "IDENTITY",
8100            Self::Indexes => "INDEXES",
8101            Self::Statistics => "STATISTICS",
8102            Self::Storage => "STORAGE",
8103            Self::All => "ALL",
8104        })
8105    }
8106}
8107
8108impl<X: Extension + Render> Render for ColumnDef<X> {
8109    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8110        self.name.render(ctx, f)?;
8111        // A SQLite typeless column omits the type entirely (`a`, then its constraints),
8112        // so the name-to-type separator is written only when a type is present.
8113        // PostgreSQL grammar order keeps STORAGE/COMPRESSION before constraints.
8114        render_column_def_tail(self, ctx, f)
8115    }
8116}
8117
8118impl<X: Extension + Render> Render for ColumnConstraint<X> {
8119    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8120        if let Some(name) = &self.name {
8121            f.write_str("CONSTRAINT ")?;
8122            name.render(ctx, f)?;
8123            // A `Bare` constraint has no element after the name, so no separating space —
8124            // the `Bare` render arm below writes nothing.
8125            if !matches!(self.option, ColumnOption::Bare { .. }) {
8126                f.write_str(" ")?;
8127            }
8128        }
8129        self.option.render(ctx, f)?;
8130        if let Some(conflict) = self.conflict {
8131            f.write_str(" ON CONFLICT ")?;
8132            conflict.render(ctx, f)?;
8133        }
8134        render_constraint_characteristics(&self.characteristics, ctx, f)
8135    }
8136}
8137
8138/// Render a trailing ` DEFERRABLE`/` INITIALLY …` characteristics clause when
8139/// present; the leading space is part of the clause separator.
8140fn render_constraint_characteristics(
8141    characteristics: &Option<Box<ConstraintCharacteristics>>,
8142    ctx: &RenderCtx<'_>,
8143    f: &mut fmt::Formatter<'_>,
8144) -> fmt::Result {
8145    if let Some(characteristics) = characteristics {
8146        f.write_str(" ")?;
8147        characteristics.render(ctx, f)?;
8148    }
8149    Ok(())
8150}
8151
8152impl Render for ConstraintCharacteristics {
8153    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8154        // A separator is needed only between two written clauses, so the second
8155        // clause carries its own leading space when the first was also present.
8156        let mut written = false;
8157        if let Some(deferrable) = self.deferrable {
8158            f.write_str(if deferrable {
8159                "DEFERRABLE"
8160            } else {
8161                "NOT DEFERRABLE"
8162            })?;
8163            written = true;
8164        }
8165        if let Some(initially_deferred) = self.initially_deferred {
8166            if written {
8167                f.write_str(" ")?;
8168            }
8169            f.write_str(if initially_deferred {
8170                "INITIALLY DEFERRED"
8171            } else {
8172                "INITIALLY IMMEDIATE"
8173            })?;
8174        }
8175        Ok(())
8176    }
8177}
8178
8179impl<X: Extension + Render> Render for ColumnOption<X> {
8180    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8181        match self {
8182            Self::Null { .. } => f.write_str("NULL"),
8183            Self::NotNull { .. } => f.write_str("NOT NULL"),
8184            Self::Default { expr, .. } => {
8185                f.write_str("DEFAULT ")?;
8186                expr.render(ctx, f)
8187            }
8188            Self::Generated { generated, .. } => generated.render(ctx, f),
8189            Self::Identity { identity, .. } => identity.render(ctx, f),
8190            Self::PrimaryKey {
8191                ascending,
8192                index_tablespace,
8193                ..
8194            } => {
8195                f.write_str("PRIMARY KEY")?;
8196                match ascending {
8197                    Some(true) => f.write_str(" ASC")?,
8198                    Some(false) => f.write_str(" DESC")?,
8199                    None => {}
8200                }
8201                render_using_index_tablespace(index_tablespace.as_deref(), ctx, f)
8202            }
8203            Self::Unique {
8204                nulls_not_distinct,
8205                index_tablespace,
8206                ..
8207            } => {
8208                f.write_str("UNIQUE")?;
8209                render_nulls_not_distinct(*nulls_not_distinct, f)?;
8210                render_using_index_tablespace(index_tablespace.as_deref(), ctx, f)
8211            }
8212            Self::AutoIncrement { spelling, .. } => spelling.render(ctx, f),
8213            Self::Collate { collation, .. } => {
8214                f.write_str("COLLATE ")?;
8215                collation.render(ctx, f)
8216            }
8217            Self::Check {
8218                expr, no_inherit, ..
8219            } => {
8220                f.write_str("CHECK (")?;
8221                expr.render(ctx, f)?;
8222                f.write_str(")")?;
8223                if *no_inherit {
8224                    f.write_str(" NO INHERIT")?;
8225                }
8226                Ok(())
8227            }
8228            Self::References { reference, .. } => reference.render(ctx, f),
8229            // No element text — the enclosing `ColumnConstraint` render skips the separating
8230            // space it would otherwise write between the name and this element.
8231            Self::Bare { .. } => Ok(()),
8232            Self::Other { ext, .. } => ext.render(ctx, f),
8233        }
8234    }
8235}
8236
8237impl Render for AutoIncrementSpelling {
8238    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8239        f.write_str(match self {
8240            Self::Underscored => "AUTO_INCREMENT",
8241            Self::Joined => "AUTOINCREMENT",
8242        })
8243    }
8244}
8245
8246impl Render for ForeignKeyRef {
8247    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8248        f.write_str("REFERENCES ")?;
8249        self.table.render(ctx, f)?;
8250        if !self.columns.is_empty() {
8251            f.write_str(" (")?;
8252            render_ident_list(&self.columns, ctx, f)?;
8253            f.write_str(")")?;
8254        }
8255        // Canonical order: MATCH, then ON DELETE, then ON UPDATE. The clauses parse
8256        // order-independently, so the delete/update order is a normalization — replayed
8257        // only by a source-fidelity render honouring the `update_before_delete` tag.
8258        if let Some(match_type) = self.match_type {
8259            f.write_str(" MATCH ")?;
8260            match_type.render(ctx, f)?;
8261        }
8262        let render_on_delete = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
8263            if let Some(on_delete) = &self.on_delete {
8264                f.write_str(" ON DELETE ")?;
8265                on_delete.render(ctx, f)?;
8266            }
8267            Ok(())
8268        };
8269        let render_on_update = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
8270            if let Some(on_update) = &self.on_update {
8271                f.write_str(" ON UPDATE ")?;
8272                on_update.render(ctx, f)?;
8273            }
8274            Ok(())
8275        };
8276        if self.update_before_delete && honours_source_spelling(ctx) {
8277            render_on_update(f)?;
8278            render_on_delete(f)?;
8279        } else {
8280            render_on_delete(f)?;
8281            render_on_update(f)?;
8282        }
8283        Ok(())
8284    }
8285}
8286
8287impl Render for ForeignKeyMatch {
8288    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8289        f.write_str(match self {
8290            Self::Full => "FULL",
8291            Self::Partial => "PARTIAL",
8292            Self::Simple => "SIMPLE",
8293        })
8294    }
8295}
8296
8297impl Render for ReferentialAction {
8298    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8299        match self {
8300            Self::NoAction { .. } => f.write_str("NO ACTION"),
8301            Self::Restrict { .. } => f.write_str("RESTRICT"),
8302            Self::Cascade { .. } => f.write_str("CASCADE"),
8303            Self::SetNull { columns, .. } => render_set_action(ctx, f, "SET NULL", columns),
8304            Self::SetDefault { columns, .. } => render_set_action(ctx, f, "SET DEFAULT", columns),
8305        }
8306    }
8307}
8308
8309/// Render `SET NULL` / `SET DEFAULT` with its optional `(col, ...)` column list.
8310fn render_set_action(
8311    ctx: &RenderCtx<'_>,
8312    f: &mut fmt::Formatter<'_>,
8313    keyword: &str,
8314    columns: &[Ident],
8315) -> fmt::Result {
8316    f.write_str(keyword)?;
8317    if !columns.is_empty() {
8318        f.write_str(" (")?;
8319        render_ident_list(columns, ctx, f)?;
8320        f.write_str(")")?;
8321    }
8322    Ok(())
8323}
8324
8325impl<X: Extension + Render> Render for GeneratedColumn<X> {
8326    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8327        // The surface tag restores the source spelling: the standard keyworded form or
8328        // the MySQL/SQLite keywordless `AS (…)` shorthand (ADR-0011).
8329        f.write_str(match self.spelling {
8330            GeneratedColumnSpelling::GeneratedAlways => "GENERATED ALWAYS AS (",
8331            GeneratedColumnSpelling::Shorthand => "AS (",
8332        })?;
8333        self.expr.render(ctx, f)?;
8334        f.write_str(")")?;
8335        if let Some(storage) = self.storage {
8336            f.write_str(" ")?;
8337            storage.render(ctx, f)?;
8338        }
8339        Ok(())
8340    }
8341}
8342
8343impl Render for GeneratedColumnStorage {
8344    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8345        f.write_str(match self {
8346            Self::Stored => "STORED",
8347            Self::Virtual => "VIRTUAL",
8348        })
8349    }
8350}
8351
8352impl<X: Extension + Render> Render for IdentityColumn<X> {
8353    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8354        f.write_str("GENERATED ")?;
8355        self.generation.render(ctx, f)?;
8356        f.write_str(" AS IDENTITY")?;
8357        if !self.options.is_empty() {
8358            f.write_str(" (")?;
8359            for (index, option) in self.options.iter().enumerate() {
8360                if index > 0 {
8361                    f.write_str(" ")?;
8362                }
8363                option.render(ctx, f)?;
8364            }
8365            f.write_str(")")?;
8366        }
8367        Ok(())
8368    }
8369}
8370
8371impl Render for IdentityGeneration {
8372    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8373        f.write_str(match self {
8374            Self::Always => "ALWAYS",
8375            Self::ByDefault => "BY DEFAULT",
8376        })
8377    }
8378}
8379
8380impl<X: Extension + Render> Render for IdentityOption<X> {
8381    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8382        match self {
8383            Self::StartWith { expr, .. } => {
8384                f.write_str("START WITH ")?;
8385                expr.render(ctx, f)
8386            }
8387            Self::IncrementBy { expr, .. } => {
8388                f.write_str("INCREMENT BY ")?;
8389                expr.render(ctx, f)
8390            }
8391            Self::MinValue {
8392                value: Some(expr), ..
8393            } => {
8394                f.write_str("MINVALUE ")?;
8395                expr.render(ctx, f)
8396            }
8397            Self::MinValue { value: None, .. } => f.write_str("NO MINVALUE"),
8398            Self::MaxValue {
8399                value: Some(expr), ..
8400            } => {
8401                f.write_str("MAXVALUE ")?;
8402                expr.render(ctx, f)
8403            }
8404            Self::MaxValue { value: None, .. } => f.write_str("NO MAXVALUE"),
8405            Self::Cache { expr, .. } => {
8406                f.write_str("CACHE ")?;
8407                expr.render(ctx, f)
8408            }
8409            Self::Cycle { cycle: true, .. } => f.write_str("CYCLE"),
8410            Self::Cycle { cycle: false, .. } => f.write_str("NO CYCLE"),
8411        }
8412    }
8413}
8414
8415impl<X: Extension + Render> Render for TableConstraintDef<X> {
8416    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8417        if let Some(name) = &self.name {
8418            f.write_str("CONSTRAINT ")?;
8419            name.render(ctx, f)?;
8420            // A `Bare` constraint has no element after the name, so no separating space —
8421            // the `Bare` render arm below writes nothing.
8422            if !matches!(self.constraint, TableConstraint::Bare { .. }) {
8423                f.write_str(" ")?;
8424            }
8425        }
8426        self.constraint.render(ctx, f)?;
8427        // The `NO INHERIT` / `NOT VALID` markers share PostgreSQL's constraint-attribute slot
8428        // with the deferral characteristics; a fixed canonical order round-trips (PostgreSQL
8429        // accepts the markers in any order).
8430        if self.no_inherit {
8431            f.write_str(" NO INHERIT")?;
8432        }
8433        if self.not_valid {
8434            f.write_str(" NOT VALID")?;
8435        }
8436        render_constraint_characteristics(&self.characteristics, ctx, f)
8437    }
8438}
8439
8440impl<X: Extension + Render> Render for TableConstraint<X> {
8441    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8442        match self {
8443            Self::PrimaryKey {
8444                columns, include, ..
8445            } => {
8446                f.write_str("PRIMARY KEY (")?;
8447                render_comma_separated(columns, ctx, f)?;
8448                f.write_str(")")?;
8449                render_include_columns(include, ctx, f)
8450            }
8451            Self::Unique {
8452                columns,
8453                nulls_not_distinct,
8454                include,
8455                ..
8456            } => {
8457                f.write_str("UNIQUE")?;
8458                render_nulls_not_distinct(*nulls_not_distinct, f)?;
8459                f.write_str(" (")?;
8460                render_comma_separated(columns, ctx, f)?;
8461                f.write_str(")")?;
8462                render_include_columns(include, ctx, f)
8463            }
8464            Self::Check { expr, .. } => {
8465                f.write_str("CHECK (")?;
8466                expr.render(ctx, f)?;
8467                f.write_str(")")
8468            }
8469            Self::Exclude { exclude, .. } => exclude.render(ctx, f),
8470            Self::ForeignKey {
8471                columns,
8472                references,
8473                ..
8474            } => {
8475                f.write_str("FOREIGN KEY (")?;
8476                render_ident_list(columns, ctx, f)?;
8477                f.write_str(") ")?;
8478                references.render(ctx, f)
8479            }
8480            // No element text — the enclosing `TableConstraintDef` render skips the separating
8481            // space it would otherwise write between the name and this element.
8482            Self::Bare { .. } => Ok(()),
8483            Self::Other { ext, .. } => ext.render(ctx, f),
8484        }
8485    }
8486}
8487
8488/// Render an `INCLUDE (<col>, ...)` covering-column list; the leading space is part of the
8489/// clause separator, and an empty list (no clause) renders nothing.
8490fn render_include_columns(
8491    include: &[Ident],
8492    ctx: &RenderCtx<'_>,
8493    f: &mut fmt::Formatter<'_>,
8494) -> fmt::Result {
8495    if !include.is_empty() {
8496        f.write_str(" INCLUDE (")?;
8497        render_ident_list(include, ctx, f)?;
8498        f.write_str(")")?;
8499    }
8500    Ok(())
8501}
8502
8503/// Render the ` NULLS [NOT] DISTINCT` null-treatment; the leading space is part of the clause
8504/// separator, and an unwritten treatment (`None`) renders nothing.
8505fn render_nulls_not_distinct(
8506    nulls_not_distinct: Option<bool>,
8507    f: &mut fmt::Formatter<'_>,
8508) -> fmt::Result {
8509    match nulls_not_distinct {
8510        Some(false) => f.write_str(" NULLS NOT DISTINCT"),
8511        Some(true) => f.write_str(" NULLS DISTINCT"),
8512        None => Ok(()),
8513    }
8514}
8515
8516/// Render the ` USING INDEX TABLESPACE <name>` index-parameter clause when present; the leading
8517/// space is part of the clause separator.
8518fn render_using_index_tablespace(
8519    tablespace: Option<&Ident>,
8520    ctx: &RenderCtx<'_>,
8521    f: &mut fmt::Formatter<'_>,
8522) -> fmt::Result {
8523    if let Some(tablespace) = tablespace {
8524        f.write_str(" USING INDEX TABLESPACE ")?;
8525        tablespace.render(ctx, f)?;
8526    }
8527    Ok(())
8528}
8529
8530impl<X: Extension + Render> Render for ExcludeConstraint<X> {
8531    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8532        f.write_str("EXCLUDE")?;
8533        if let Some(method) = &self.method {
8534            f.write_str(" USING ")?;
8535            method.render(ctx, f)?;
8536        }
8537        f.write_str(" (")?;
8538        render_comma_separated(&self.elements, ctx, f)?;
8539        f.write_str(")")?;
8540        render_include_columns(&self.include, ctx, f)?;
8541        if !self.with_params.is_empty() {
8542            f.write_str(" WITH (")?;
8543            render_comma_separated(&self.with_params, ctx, f)?;
8544            f.write_str(")")?;
8545        }
8546        render_using_index_tablespace(self.index_tablespace.as_ref(), ctx, f)?;
8547        if let Some(predicate) = &self.predicate {
8548            f.write_str(" WHERE (")?;
8549            predicate.render(ctx, f)?;
8550            f.write_str(")")?;
8551        }
8552        Ok(())
8553    }
8554}
8555
8556impl<X: Extension + Render> Render for ExcludeElement<X> {
8557    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8558        // A parenthesized key re-adds the grammar-mandated wrapping parentheses; a bare column /
8559        // function-call key renders unwrapped, exactly as `PartitionElem`.
8560        if self.parenthesized {
8561            f.write_str("(")?;
8562            self.expr.render(ctx, f)?;
8563            f.write_str(")")?;
8564        } else {
8565            self.expr.render(ctx, f)?;
8566        }
8567        if let Some(collation) = &self.collation {
8568            f.write_str(" COLLATE ")?;
8569            collation.render(ctx, f)?;
8570        }
8571        if let Some(opclass) = &self.opclass {
8572            f.write_str(" ")?;
8573            opclass.render(ctx, f)?;
8574            if !self.opclass_params.is_empty() {
8575                f.write_str(" (")?;
8576                render_comma_separated(&self.opclass_params, ctx, f)?;
8577                f.write_str(")")?;
8578            }
8579        }
8580        match self.asc {
8581            Some(true) => f.write_str(" ASC")?,
8582            Some(false) => f.write_str(" DESC")?,
8583            None => {}
8584        }
8585        match self.nulls_first {
8586            Some(true) => f.write_str(" NULLS FIRST")?,
8587            Some(false) => f.write_str(" NULLS LAST")?,
8588            None => {}
8589        }
8590        f.write_str(" WITH ")?;
8591        self.operator.render(ctx, f)
8592    }
8593}
8594
8595impl Render for ExcludeOperator {
8596    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8597        match self.spelling {
8598            // The bare form is always unqualified: `a WITH &&`.
8599            NamedOperatorSpelling::Bare => f.write_str(ctx.resolve(self.op)),
8600            // The `OPERATOR(...)` keyword form carries the optional schema: `a WITH
8601            // OPERATOR(pg_catalog.=)`.
8602            NamedOperatorSpelling::OperatorKeyword => {
8603                f.write_str("OPERATOR(")?;
8604                for part in &self.schema.0 {
8605                    part.render(ctx, f)?;
8606                    f.write_str(".")?;
8607                }
8608                f.write_str(ctx.resolve(self.op))?;
8609                f.write_str(")")
8610            }
8611        }
8612    }
8613}
8614
8615impl<X: Extension + Render> Render for CreateTableOption<X> {
8616    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8617        self.kind.render(ctx, f)
8618    }
8619}
8620
8621impl<X: Extension + Render> Render for CreateTableOptionKind<X> {
8622    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8623        match self {
8624            Self::With { params, .. } => {
8625                f.write_str("WITH (")?;
8626                render_comma_separated(params, ctx, f)?;
8627                f.write_str(")")
8628            }
8629            Self::OnCommit { action, .. } => {
8630                f.write_str("ON COMMIT ")?;
8631                action.render(ctx, f)
8632            }
8633            Self::Tablespace { tablespace, .. } => {
8634                f.write_str("TABLESPACE ")?;
8635                tablespace.render(ctx, f)
8636            }
8637            Self::KeyValue { option, .. } => option.render(ctx, f),
8638            Self::WithoutRowid { .. } => f.write_str("WITHOUT ROWID"),
8639            Self::Strict { .. } => f.write_str("STRICT"),
8640            Self::WithoutOids { .. } => f.write_str("WITHOUT OIDS"),
8641        }
8642    }
8643}
8644
8645impl Render for TableOption {
8646    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8647        // Canonical MySQL spelling: `<name> = <value>`. The optional source `=` and
8648        // the `DEFAULT` noise prefix are normalized to this fixed form (ADR-0011); the
8649        // ` = ` matches the `WITH` storage-parameter rendering.
8650        self.name.render(ctx, f)?;
8651        f.write_str(" = ")?;
8652        self.value.render(ctx, f)
8653    }
8654}
8655
8656impl Render for TableOptionValue {
8657    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8658        match self {
8659            Self::Word { word, .. } => word.render(ctx, f),
8660            Self::String { value, .. } | Self::Number { value, .. } => value.render(ctx, f),
8661        }
8662    }
8663}
8664
8665impl Render for OnCommitAction {
8666    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8667        f.write_str(match self {
8668            Self::PreserveRows => "PRESERVE ROWS",
8669            Self::DeleteRows => "DELETE ROWS",
8670            Self::Drop => "DROP",
8671        })
8672    }
8673}
8674
8675impl<X: Extension + Render> Render for TableStorageParameter<X> {
8676    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8677        self.name.render(ctx, f)?;
8678        if let Some(value) = &self.value {
8679            f.write_str(" = ")?;
8680            value.render(ctx, f)?;
8681        }
8682        Ok(())
8683    }
8684}
8685
8686impl<X: Extension + Render> Render for AlterTable<X> {
8687    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8688        f.write_str("ALTER TABLE")?;
8689        if self.if_exists {
8690            f.write_str(" IF EXISTS")?;
8691        }
8692        f.write_str(" ")?;
8693        self.name.render(ctx, f)?;
8694        f.write_str(" ")?;
8695        render_comma_separated(&self.actions, ctx, f)
8696    }
8697}
8698
8699impl Render for AlterColumnTarget {
8700    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8701        for (i, part) in self.parts.iter().enumerate() {
8702            if i > 0 {
8703                f.write_str(".")?;
8704            }
8705            part.render(ctx, f)?;
8706        }
8707        Ok(())
8708    }
8709}
8710
8711/// Render the optional `COLUMN` noise word (with its leading space) after an
8712/// `ADD`/`DROP`/`ALTER`/`RENAME` column action. The canonical render always emits it;
8713/// a source-fidelity render drops it when the source did (exact-synonym fidelity).
8714fn render_optional_column_keyword(
8715    column_keyword: bool,
8716    ctx: &RenderCtx<'_>,
8717    f: &mut fmt::Formatter<'_>,
8718) -> fmt::Result {
8719    if column_keyword || !honours_source_spelling(ctx) {
8720        f.write_str(" COLUMN")?;
8721    }
8722    Ok(())
8723}
8724
8725fn render_column_def_tail<X: Extension + Render>(
8726    column: &ColumnDef<X>,
8727    ctx: &RenderCtx<'_>,
8728    f: &mut fmt::Formatter<'_>,
8729) -> fmt::Result {
8730    if let Some(data_type) = &column.data_type {
8731        f.write_str(" ")?;
8732        data_type.render(ctx, f)?;
8733    }
8734    if let Some(storage) = &column.storage {
8735        f.write_str(" STORAGE ")?;
8736        storage.render(ctx, f)?;
8737    }
8738    if let Some(compression) = &column.compression {
8739        f.write_str(" COMPRESSION ")?;
8740        compression.render(ctx, f)?;
8741    }
8742    for constraint in &column.constraints {
8743        f.write_str(" ")?;
8744        constraint.render(ctx, f)?;
8745    }
8746    Ok(())
8747}
8748
8749impl<X: Extension + Render> Render for AlterTableAction<X> {
8750    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8751        match self {
8752            // The optional `COLUMN` noise word is exact-synonym fidelity: the canonical
8753            // render emits it, a source-fidelity render drops it when the source did.
8754            Self::AddColumn {
8755                if_not_exists,
8756                column_keyword,
8757                target,
8758                column,
8759                ..
8760            } => {
8761                f.write_str("ADD")?;
8762                render_optional_column_keyword(*column_keyword, ctx, f)?;
8763                if *if_not_exists {
8764                    f.write_str(" IF NOT EXISTS")?;
8765                }
8766                f.write_str(" ")?;
8767                if let Some(target) = target {
8768                    target.render(ctx, f)?;
8769                    render_column_def_tail(column, ctx, f)
8770                } else {
8771                    column.render(ctx, f)
8772                }
8773            }
8774            Self::DropColumn {
8775                if_exists,
8776                column_keyword,
8777                name,
8778                behavior,
8779                ..
8780            } => {
8781                f.write_str("DROP")?;
8782                render_optional_column_keyword(*column_keyword, ctx, f)?;
8783                if *if_exists {
8784                    f.write_str(" IF EXISTS")?;
8785                }
8786                f.write_str(" ")?;
8787                name.render(ctx, f)?;
8788                render_drop_behavior(*behavior, ctx, f)
8789            }
8790            Self::AlterColumn {
8791                column_keyword,
8792                name,
8793                change,
8794                ..
8795            } => {
8796                f.write_str("ALTER")?;
8797                render_optional_column_keyword(*column_keyword, ctx, f)?;
8798                f.write_str(" ")?;
8799                name.render(ctx, f)?;
8800                f.write_str(" ")?;
8801                change.render(ctx, f)
8802            }
8803            Self::AddConstraint { constraint, .. } => {
8804                f.write_str("ADD ")?;
8805                constraint.render(ctx, f)
8806            }
8807            Self::DropConstraint {
8808                if_exists,
8809                name,
8810                behavior,
8811                ..
8812            } => {
8813                f.write_str("DROP CONSTRAINT")?;
8814                if *if_exists {
8815                    f.write_str(" IF EXISTS")?;
8816                }
8817                f.write_str(" ")?;
8818                name.render(ctx, f)?;
8819                render_drop_behavior(*behavior, ctx, f)
8820            }
8821            Self::RenameColumn {
8822                column_keyword,
8823                name,
8824                new_name,
8825                ..
8826            } => {
8827                f.write_str("RENAME")?;
8828                render_optional_column_keyword(*column_keyword, ctx, f)?;
8829                f.write_str(" ")?;
8830                name.render(ctx, f)?;
8831                f.write_str(" TO ")?;
8832                new_name.render(ctx, f)
8833            }
8834            Self::RenameTable { new_name, .. } => {
8835                f.write_str("RENAME TO ")?;
8836                new_name.render(ctx, f)
8837            }
8838            Self::AttachPartition {
8839                partition, bound, ..
8840            } => {
8841                f.write_str("ATTACH PARTITION ")?;
8842                partition.render(ctx, f)?;
8843                f.write_str(" ")?;
8844                bound.render(ctx, f)
8845            }
8846            Self::DetachPartition {
8847                partition, mode, ..
8848            } => {
8849                f.write_str("DETACH PARTITION ")?;
8850                partition.render(ctx, f)?;
8851                match mode {
8852                    Some(DetachPartitionMode::Concurrently) => f.write_str(" CONCURRENTLY"),
8853                    Some(DetachPartitionMode::Finalize) => f.write_str(" FINALIZE"),
8854                    None => Ok(()),
8855                }
8856            }
8857        }
8858    }
8859}
8860
8861impl<X: Extension + Render> Render for AlterColumnAction<X> {
8862    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8863        match self {
8864            Self::SetDefault { expr, .. } => {
8865                f.write_str("SET DEFAULT ")?;
8866                expr.render(ctx, f)
8867            }
8868            Self::DropDefault { .. } => f.write_str("DROP DEFAULT"),
8869            Self::SetNotNull { .. } => f.write_str("SET NOT NULL"),
8870            Self::DropNotNull { .. } => f.write_str("DROP NOT NULL"),
8871            // The ANSI `SET DATA TYPE` spelling is canonical; a source-fidelity render
8872            // replays the bare PostgreSQL `TYPE` when the source wrote it.
8873            Self::SetDataType {
8874                set_data,
8875                data_type,
8876                using,
8877                ..
8878            } => {
8879                if *set_data || !honours_source_spelling(ctx) {
8880                    f.write_str("SET DATA TYPE ")?;
8881                } else {
8882                    f.write_str("TYPE ")?;
8883                }
8884                data_type.render(ctx, f)?;
8885                if let Some(using) = using {
8886                    f.write_str(" USING ")?;
8887                    using.render(ctx, f)?;
8888                }
8889                Ok(())
8890            }
8891        }
8892    }
8893}
8894
8895impl Render for DropStatement {
8896    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8897        f.write_str("DROP ")?;
8898        self.object_kind.render(ctx, f)?;
8899        if self.if_exists {
8900            f.write_str(" IF EXISTS")?;
8901        }
8902        f.write_str(" ")?;
8903        render_comma_separated(&self.names, ctx, f)?;
8904        render_drop_behavior(self.behavior, ctx, f)
8905    }
8906}
8907
8908impl Render for DropObjectKind {
8909    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8910        f.write_str(match self {
8911            Self::Table => "TABLE",
8912            Self::View => "VIEW",
8913            Self::MaterializedView => "MATERIALIZED VIEW",
8914            Self::Index => "INDEX",
8915            Self::Schema => "SCHEMA",
8916            Self::Type => "TYPE",
8917            Self::Sequence => "SEQUENCE",
8918            Self::Macro => "MACRO",
8919            Self::MacroTable => "MACRO TABLE",
8920            Self::Trigger => "TRIGGER",
8921        })
8922    }
8923}
8924
8925impl Render for DropBehavior {
8926    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8927        f.write_str(match self {
8928            Self::Cascade => "CASCADE",
8929            Self::Restrict => "RESTRICT",
8930        })
8931    }
8932}
8933
8934/// Render a trailing ` CASCADE` / ` RESTRICT` drop behaviour when present; the
8935/// leading space is part of the clause separator.
8936fn render_drop_behavior(
8937    behavior: Option<DropBehavior>,
8938    ctx: &RenderCtx<'_>,
8939    f: &mut fmt::Formatter<'_>,
8940) -> fmt::Result {
8941    if let Some(behavior) = behavior {
8942        f.write_str(" ")?;
8943        behavior.render(ctx, f)?;
8944    }
8945    Ok(())
8946}
8947
8948impl<X: Extension + Render> Render for CreateSchema<X> {
8949    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8950        f.write_str("CREATE SCHEMA")?;
8951        if self.if_not_exists {
8952            f.write_str(" IF NOT EXISTS")?;
8953        }
8954        if let Some(name) = &self.name {
8955            f.write_str(" ")?;
8956            name.render(ctx, f)?;
8957        }
8958        if let Some(authorization) = &self.authorization {
8959            f.write_str(" AUTHORIZATION ")?;
8960            authorization.render(ctx, f)?;
8961        }
8962        // Embedded schema elements render space-separated after the head (no `;`), so
8963        // the whole `CREATE SCHEMA s CREATE TABLE t ...` stays one rendered statement —
8964        // preserving the statement count and round-tripping through the parser's
8965        // schema-element loop.
8966        for element in &self.elements {
8967            f.write_str(" ")?;
8968            element.render(ctx, f)?;
8969        }
8970        Ok(())
8971    }
8972}
8973
8974impl<X: Extension + Render> Render for CreateView<X> {
8975    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8976        f.write_str("CREATE")?;
8977        if self.or_replace {
8978            f.write_str(" OR REPLACE")?;
8979        }
8980        if let Some(temporary) = self.temporary {
8981            f.write_str(" ")?;
8982            temporary.render(ctx, f)?;
8983        }
8984        if self.materialized {
8985            f.write_str(" MATERIALIZED")?;
8986        }
8987        // `RECURSIVE` sits between the `TEMP`/`TEMPORARY` prefix and `VIEW`; it never
8988        // co-occurs with `MATERIALIZED` (parser-enforced), so the order is unambiguous.
8989        if self.recursive {
8990            f.write_str(" RECURSIVE")?;
8991        }
8992        // The MySQL `[ALGORITHM = …] [DEFINER = …] [SQL SECURITY …]` prefix, between `OR
8993        // REPLACE` and `VIEW`. All-`None` (every non-MySQL view) emits nothing.
8994        self.options.render(ctx, f)?;
8995        f.write_str(" VIEW")?;
8996        if self.if_not_exists {
8997            f.write_str(" IF NOT EXISTS")?;
8998        }
8999        f.write_str(" ")?;
9000        self.name.render(ctx, f)?;
9001        if !self.columns.is_empty() {
9002            f.write_str(" (")?;
9003            render_ident_list(&self.columns, ctx, f)?;
9004            f.write_str(")")?;
9005        }
9006        f.write_str(" AS ")?;
9007        self.query.render(ctx, f)?;
9008        if let Some(check_option) = self.check_option {
9009            f.write_str(" ")?;
9010            check_option.render(ctx, f)?;
9011        }
9012        render_with_data(self.with_data, f)
9013    }
9014}
9015
9016impl<X: Extension + Render> Render for AlterView<X> {
9017    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9018        f.write_str("ALTER")?;
9019        // The `[ALGORITHM = …] [DEFINER = …] [SQL SECURITY …]` prefix, between `ALTER` and
9020        // `VIEW`. All-`None` (a bare `ALTER VIEW`) emits nothing.
9021        self.options.render(ctx, f)?;
9022        f.write_str(" VIEW ")?;
9023        self.name.render(ctx, f)?;
9024        if !self.columns.is_empty() {
9025            f.write_str(" (")?;
9026            render_ident_list(&self.columns, ctx, f)?;
9027            f.write_str(")")?;
9028        }
9029        f.write_str(" AS ")?;
9030        self.query.render(ctx, f)?;
9031        if let Some(check_option) = self.check_option {
9032            f.write_str(" ")?;
9033            check_option.render(ctx, f)?;
9034        }
9035        Ok(())
9036    }
9037}
9038
9039impl Render for ViewOptions {
9040    /// Emit each present option with a *leading* space, so the prefix slots between the
9041    /// `CREATE`/`ALTER` keyword and `VIEW`. All-`None` emits nothing (every non-MySQL view).
9042    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9043        if let Some(algorithm) = self.algorithm {
9044            f.write_str(" ALGORITHM = ")?;
9045            algorithm.render(ctx, f)?;
9046        }
9047        if let Some(definer) = &self.definer {
9048            f.write_str(" ")?;
9049            definer.render(ctx, f)?;
9050        }
9051        if let Some(sql_security) = self.sql_security {
9052            f.write_str(" SQL SECURITY ")?;
9053            sql_security.render(ctx, f)?;
9054        }
9055        Ok(())
9056    }
9057}
9058
9059impl Render for ViewAlgorithm {
9060    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9061        f.write_str(match self {
9062            Self::Undefined => "UNDEFINED",
9063            Self::Merge => "MERGE",
9064            Self::TempTable => "TEMPTABLE",
9065        })
9066    }
9067}
9068
9069impl<X: Extension + Render> Render for CreateSecret<X> {
9070    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9071        f.write_str("CREATE ")?;
9072        if self.persistent {
9073            f.write_str("PERSISTENT ")?;
9074        }
9075        f.write_str("SECRET ")?;
9076        self.name.render(ctx, f)?;
9077        f.write_str(" (")?;
9078        render_comma_separated(&self.options, ctx, f)?;
9079        f.write_str(")")
9080    }
9081}
9082
9083impl<X: Extension + Render> Render for SecretOption<X> {
9084    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9085        self.name.render(ctx, f)?;
9086        f.write_str(" ")?;
9087        self.value.render(ctx, f)
9088    }
9089}
9090
9091impl Render for DropSecretStmt {
9092    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9093        f.write_str("DROP ")?;
9094        match self.persistence {
9095            SecretPersistence::Default => {}
9096            SecretPersistence::Temporary => f.write_str("TEMPORARY ")?,
9097            SecretPersistence::Persistent => f.write_str("PERSISTENT ")?,
9098        }
9099        f.write_str("SECRET ")?;
9100        if self.if_exists {
9101            f.write_str("IF EXISTS ")?;
9102        }
9103        self.name.render(ctx, f)?;
9104        if let Some(storage) = &self.storage {
9105            f.write_str(" FROM ")?;
9106            storage.render(ctx, f)?;
9107        }
9108        Ok(())
9109    }
9110}
9111
9112impl<X: Extension + Render> Render for CreateType<X> {
9113    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9114        f.write_str("CREATE")?;
9115        if self.or_replace {
9116            f.write_str(" OR REPLACE")?;
9117        }
9118        if let Some(temporary) = self.temporary {
9119            f.write_str(" ")?;
9120            temporary.render(ctx, f)?;
9121        }
9122        f.write_str(" TYPE")?;
9123        if self.if_not_exists {
9124            f.write_str(" IF NOT EXISTS")?;
9125        }
9126        f.write_str(" ")?;
9127        self.name.render(ctx, f)?;
9128        f.write_str(" AS ")?;
9129        self.definition.render(ctx, f)
9130    }
9131}
9132
9133impl Render for CreateVirtualTable {
9134    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9135        f.write_str("CREATE VIRTUAL TABLE ")?;
9136        if self.if_not_exists {
9137            f.write_str("IF NOT EXISTS ")?;
9138        }
9139        self.name.render(ctx, f)?;
9140        f.write_str(" USING ")?;
9141        self.module.render(ctx, f)?;
9142        if let Some(args) = &self.args {
9143            f.write_str("(")?;
9144            render_comma_separated(args, ctx, f)?;
9145            f.write_str(")")?;
9146        }
9147        Ok(())
9148    }
9149}
9150
9151impl Render for ModuleArg {
9152    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9153        // The verbatim slice is opaque module-owned content (column names, quoted
9154        // option strings) that can carry PII, so Redacted rendering masks each argument
9155        // to one placeholder — arg arity (query shape) survives, the text does not
9156        // (ADR-0010), mirroring how `Literal` masks values to `?`.
9157        if ctx.mode() == RenderMode::Redacted {
9158            return f.write_str("?");
9159        }
9160        // Otherwise emit the interned source slice as-is: the module (not this parser)
9161        // owns the argument grammar, so the text round-trips verbatim.
9162        f.write_str(ctx.resolve(self.text))
9163    }
9164}
9165
9166impl<X: Extension + Render> Render for CreateSequence<X> {
9167    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9168        f.write_str("CREATE")?;
9169        if let Some(temporary) = self.temporary {
9170            f.write_str(" ")?;
9171            temporary.render(ctx, f)?;
9172        }
9173        f.write_str(" SEQUENCE")?;
9174        if self.if_not_exists {
9175            f.write_str(" IF NOT EXISTS")?;
9176        }
9177        f.write_str(" ")?;
9178        self.name.render(ctx, f)?;
9179        // The options are space-separated in the canonical `START WITH`/`INCREMENT BY`
9180        // spelling (which both engines accept); reuse the shared `IdentityOption` render.
9181        for option in &self.options {
9182            f.write_str(" ")?;
9183            option.render(ctx, f)?;
9184        }
9185        Ok(())
9186    }
9187}
9188
9189impl Render for CreateExtension {
9190    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9191        f.write_str("CREATE EXTENSION")?;
9192        if self.if_not_exists {
9193            f.write_str(" IF NOT EXISTS")?;
9194        }
9195        f.write_str(" ")?;
9196        self.name.render(ctx, f)?;
9197        // `WITH` is optional sugar; replay it only when the source wrote it.
9198        if self.with {
9199            f.write_str(" WITH")?;
9200        }
9201        for option in &self.options {
9202            f.write_str(" ")?;
9203            option.render(ctx, f)?;
9204        }
9205        Ok(())
9206    }
9207}
9208
9209impl Render for CreateExtensionOption {
9210    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9211        match self {
9212            Self::Schema { name, .. } => {
9213                f.write_str("SCHEMA ")?;
9214                name.render(ctx, f)
9215            }
9216            Self::Version { version, .. } => {
9217                f.write_str("VERSION ")?;
9218                version.render(ctx, f)
9219            }
9220            Self::Cascade { .. } => f.write_str("CASCADE"),
9221        }
9222    }
9223}
9224
9225impl Render for ExtensionVersion {
9226    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9227        match self {
9228            Self::Word { word, .. } => word.render(ctx, f),
9229            Self::String { value, .. } => value.render(ctx, f),
9230        }
9231    }
9232}
9233
9234impl<X: Extension + Render> Render for AlterExtension<X> {
9235    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9236        f.write_str("ALTER EXTENSION ")?;
9237        self.name.render(ctx, f)?;
9238        f.write_str(" ")?;
9239        self.action.render(ctx, f)
9240    }
9241}
9242
9243impl<X: Extension + Render> Render for AlterExtensionAction<X> {
9244    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9245        match self {
9246            Self::Update { version, .. } => {
9247                f.write_str("UPDATE")?;
9248                if let Some(version) = version {
9249                    f.write_str(" TO ")?;
9250                    version.render(ctx, f)?;
9251                }
9252                Ok(())
9253            }
9254            Self::Change { add, member, .. } => {
9255                f.write_str(if *add { "ADD " } else { "DROP " })?;
9256                member.render(ctx, f)
9257            }
9258        }
9259    }
9260}
9261
9262impl Render for SizeLiteral {
9263    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9264        // The exact source spelling (digits + any suffix, and its case) round-trips from the
9265        // whole-literal span, exactly as a `Literal` does.
9266        if let Some(text) = ctx.slice(self.meta.span) {
9267            return f.write_str(text);
9268        }
9269        // A synthesized or detached literal has no backing source; fall back to a
9270        // placeholder of the tagged unit so rendering stays total.
9271        f.write_str("0")?;
9272        match self.unit {
9273            Some(SizeUnit::Kilo) => f.write_str("K"),
9274            Some(SizeUnit::Mega) => f.write_str("M"),
9275            Some(SizeUnit::Giga) => f.write_str("G"),
9276            None => Ok(()),
9277        }
9278    }
9279}
9280
9281impl Render for TablespaceOption {
9282    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9283        match self {
9284            Self::Size {
9285                kind, equals, size, ..
9286            } => {
9287                f.write_str(match kind {
9288                    TablespaceSizeOption::InitialSize => "INITIAL_SIZE",
9289                    TablespaceSizeOption::AutoextendSize => "AUTOEXTEND_SIZE",
9290                    TablespaceSizeOption::MaxSize => "MAX_SIZE",
9291                    TablespaceSizeOption::ExtentSize => "EXTENT_SIZE",
9292                    TablespaceSizeOption::UndoBufferSize => "UNDO_BUFFER_SIZE",
9293                    TablespaceSizeOption::RedoBufferSize => "REDO_BUFFER_SIZE",
9294                    TablespaceSizeOption::FileBlockSize => "FILE_BLOCK_SIZE",
9295                })?;
9296                f.write_str(if *equals { " = " } else { " " })?;
9297                size.render(ctx, f)
9298            }
9299            Self::Nodegroup { equals, value, .. } => {
9300                f.write_str("NODEGROUP")?;
9301                f.write_str(if *equals { " = " } else { " " })?;
9302                value.render(ctx, f)
9303            }
9304            Self::Engine {
9305                storage,
9306                equals,
9307                name,
9308                ..
9309            } => {
9310                f.write_str(if *storage { "STORAGE ENGINE" } else { "ENGINE" })?;
9311                f.write_str(if *equals { " = " } else { " " })?;
9312                name.render(ctx, f)
9313            }
9314            Self::Wait { negated, .. } => f.write_str(if *negated { "NO_WAIT" } else { "WAIT" }),
9315            Self::Comment { equals, value, .. } => {
9316                f.write_str("COMMENT")?;
9317                f.write_str(if *equals { " = " } else { " " })?;
9318                value.render(ctx, f)
9319            }
9320            Self::Encryption { equals, value, .. } => {
9321                f.write_str("ENCRYPTION")?;
9322                f.write_str(if *equals { " = " } else { " " })?;
9323                value.render(ctx, f)
9324            }
9325            Self::EngineAttribute { equals, value, .. } => {
9326                f.write_str("ENGINE_ATTRIBUTE")?;
9327                f.write_str(if *equals { " = " } else { " " })?;
9328                value.render(ctx, f)
9329            }
9330        }
9331    }
9332}
9333
9334/// Render a trailing option list, each option preceded by a single space (the canonical form
9335/// space-separates; the source `opt_comma` separators are not replayed). Shared by every
9336/// tablespace / logfile-group render.
9337fn render_tablespace_options(
9338    options: &[TablespaceOption],
9339    ctx: &RenderCtx<'_>,
9340    f: &mut fmt::Formatter<'_>,
9341) -> fmt::Result {
9342    for option in options {
9343        f.write_str(" ")?;
9344        option.render(ctx, f)?;
9345    }
9346    Ok(())
9347}
9348
9349impl Render for CreateTablespace {
9350    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9351        f.write_str(if self.undo {
9352            "CREATE UNDO TABLESPACE "
9353        } else {
9354            "CREATE TABLESPACE "
9355        })?;
9356        self.name.render(ctx, f)?;
9357        if let Some(datafile) = &self.datafile {
9358            f.write_str(" ADD DATAFILE ")?;
9359            datafile.render(ctx, f)?;
9360        }
9361        if let Some(lg) = &self.use_logfile_group {
9362            f.write_str(" USE LOGFILE GROUP ")?;
9363            lg.render(ctx, f)?;
9364        }
9365        render_tablespace_options(&self.options, ctx, f)
9366    }
9367}
9368
9369impl Render for AlterTablespace {
9370    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9371        // The `ALTER UNDO TABLESPACE` head is used exactly when the action is `SetState`.
9372        f.write_str(
9373            if matches!(self.action, AlterTablespaceAction::SetState { .. }) {
9374                "ALTER UNDO TABLESPACE "
9375            } else {
9376                "ALTER TABLESPACE "
9377            },
9378        )?;
9379        self.name.render(ctx, f)?;
9380        f.write_str(" ")?;
9381        self.action.render(ctx, f)
9382    }
9383}
9384
9385impl Render for AlterTablespaceAction {
9386    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9387        match self {
9388            Self::AddDatafile {
9389                datafile, options, ..
9390            } => {
9391                f.write_str("ADD DATAFILE ")?;
9392                datafile.render(ctx, f)?;
9393                render_tablespace_options(options, ctx, f)
9394            }
9395            Self::DropDatafile {
9396                datafile, options, ..
9397            } => {
9398                f.write_str("DROP DATAFILE ")?;
9399                datafile.render(ctx, f)?;
9400                render_tablespace_options(options, ctx, f)
9401            }
9402            Self::Rename { new_name, .. } => {
9403                f.write_str("RENAME TO ")?;
9404                new_name.render(ctx, f)
9405            }
9406            Self::Options { options, .. } => {
9407                // The bare option list is non-empty; render the first without a leading space,
9408                // the rest space-separated.
9409                let mut first = true;
9410                for option in options {
9411                    if !first {
9412                        f.write_str(" ")?;
9413                    }
9414                    first = false;
9415                    option.render(ctx, f)?;
9416                }
9417                Ok(())
9418            }
9419            Self::SetState { state, options, .. } => {
9420                f.write_str(match state {
9421                    UndoTablespaceState::Active => "SET ACTIVE",
9422                    UndoTablespaceState::Inactive => "SET INACTIVE",
9423                })?;
9424                render_tablespace_options(options, ctx, f)
9425            }
9426        }
9427    }
9428}
9429
9430impl Render for DropTablespace {
9431    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9432        f.write_str(if self.undo {
9433            "DROP UNDO TABLESPACE "
9434        } else {
9435            "DROP TABLESPACE "
9436        })?;
9437        self.name.render(ctx, f)?;
9438        render_tablespace_options(&self.options, ctx, f)
9439    }
9440}
9441
9442impl Render for CreateLogfileGroup {
9443    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9444        f.write_str("CREATE LOGFILE GROUP ")?;
9445        self.name.render(ctx, f)?;
9446        f.write_str(" ADD UNDOFILE ")?;
9447        self.undofile.render(ctx, f)?;
9448        render_tablespace_options(&self.options, ctx, f)
9449    }
9450}
9451
9452impl Render for AlterLogfileGroup {
9453    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9454        f.write_str("ALTER LOGFILE GROUP ")?;
9455        self.name.render(ctx, f)?;
9456        f.write_str(" ADD UNDOFILE ")?;
9457        self.undofile.render(ctx, f)?;
9458        render_tablespace_options(&self.options, ctx, f)
9459    }
9460}
9461
9462impl Render for DropLogfileGroup {
9463    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9464        f.write_str("DROP LOGFILE GROUP ")?;
9465        self.name.render(ctx, f)?;
9466        render_tablespace_options(&self.options, ctx, f)
9467    }
9468}
9469
9470impl<X: Extension + Render> Render for ObjectReference<X> {
9471    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9472        match self {
9473            Self::Named { kind, name, .. } => {
9474                kind.render(ctx, f)?;
9475                f.write_str(" ")?;
9476                name.render(ctx, f)
9477            }
9478            Self::Routine {
9479                kind, signature, ..
9480            } => {
9481                kind.render(ctx, f)?;
9482                f.write_str(" ")?;
9483                signature.render(ctx, f)
9484            }
9485            Self::Aggregate { name, args, .. } => {
9486                f.write_str("AGGREGATE ")?;
9487                name.render(ctx, f)?;
9488                args.render(ctx, f)
9489            }
9490            Self::Operator {
9491                schema, op, args, ..
9492            } => {
9493                f.write_str("OPERATOR ")?;
9494                for part in &schema.0 {
9495                    part.render(ctx, f)?;
9496                    f.write_str(".")?;
9497                }
9498                f.write_str(ctx.resolve(*op))?;
9499                f.write_str(" ")?;
9500                args.render(ctx, f)
9501            }
9502            Self::OperatorClass {
9503                family,
9504                name,
9505                access_method,
9506                ..
9507            } => {
9508                f.write_str(if *family {
9509                    "OPERATOR FAMILY "
9510                } else {
9511                    "OPERATOR CLASS "
9512                })?;
9513                name.render(ctx, f)?;
9514                f.write_str(" USING ")?;
9515                access_method.render(ctx, f)
9516            }
9517            Self::Cast { from, to, .. } => {
9518                f.write_str("CAST (")?;
9519                from.render(ctx, f)?;
9520                f.write_str(" AS ")?;
9521                to.render(ctx, f)?;
9522                f.write_str(")")
9523            }
9524            Self::Type { domain, name, .. } => {
9525                f.write_str(if *domain { "DOMAIN " } else { "TYPE " })?;
9526                name.render(ctx, f)
9527            }
9528            Self::Transform {
9529                type_name,
9530                language,
9531                ..
9532            } => {
9533                f.write_str("TRANSFORM FOR ")?;
9534                type_name.render(ctx, f)?;
9535                f.write_str(" LANGUAGE ")?;
9536                language.render(ctx, f)
9537            }
9538            Self::Trigger { name, table, .. } => {
9539                f.write_str("TRIGGER ")?;
9540                name.render(ctx, f)?;
9541                f.write_str(" ON ")?;
9542                table.render(ctx, f)
9543            }
9544        }
9545    }
9546}
9547
9548impl<X: Extension + Render> Render for AlterObjectDepends<X> {
9549    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9550        f.write_str("ALTER ")?;
9551        self.object.render(ctx, f)?;
9552        if self.no {
9553            f.write_str(" NO")?;
9554        }
9555        f.write_str(" DEPENDS ON EXTENSION ")?;
9556        self.extension.render(ctx, f)
9557    }
9558}
9559
9560impl<X: Extension + Render> Render for DropTransform<X> {
9561    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9562        // The `IF EXISTS` guard sits between `TRANSFORM` and `FOR`, so the transform
9563        // reference's `TRANSFORM FOR type LANGUAGE lang` render can't be emitted as one
9564        // unit — the parts are spelled out here around the guard. The object is always the
9565        // `Transform` variant (the parser builds no other for this node).
9566        let ObjectReference::Transform {
9567            type_name,
9568            language,
9569            ..
9570        } = &self.object
9571        else {
9572            unreachable!("DROP TRANSFORM always carries an ObjectReference::Transform")
9573        };
9574        f.write_str("DROP TRANSFORM")?;
9575        if self.if_exists {
9576            f.write_str(" IF EXISTS")?;
9577        }
9578        f.write_str(" FOR ")?;
9579        type_name.render(ctx, f)?;
9580        f.write_str(" LANGUAGE ")?;
9581        language.render(ctx, f)?;
9582        render_drop_behavior(self.behavior, ctx, f)
9583    }
9584}
9585
9586impl Render for ObjectRefKind {
9587    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9588        f.write_str(match self {
9589            Self::Table => "TABLE",
9590            Self::Sequence => "SEQUENCE",
9591            Self::View => "VIEW",
9592            Self::MaterializedView => "MATERIALIZED VIEW",
9593            Self::Index => "INDEX",
9594            Self::ForeignTable => "FOREIGN TABLE",
9595            Self::Collation => "COLLATION",
9596            Self::Conversion => "CONVERSION",
9597            Self::Statistics => "STATISTICS",
9598            Self::TextSearchParser => "TEXT SEARCH PARSER",
9599            Self::TextSearchDictionary => "TEXT SEARCH DICTIONARY",
9600            Self::TextSearchTemplate => "TEXT SEARCH TEMPLATE",
9601            Self::TextSearchConfiguration => "TEXT SEARCH CONFIGURATION",
9602            Self::AccessMethod => "ACCESS METHOD",
9603            Self::EventTrigger => "EVENT TRIGGER",
9604            Self::Extension => "EXTENSION",
9605            Self::ForeignDataWrapper => "FOREIGN DATA WRAPPER",
9606            // The `PROCEDURAL` prefix is exact-synonym sugar; the canonical render drops it.
9607            Self::Language => "LANGUAGE",
9608            Self::Publication => "PUBLICATION",
9609            Self::Schema => "SCHEMA",
9610            Self::Server => "SERVER",
9611            Self::Database => "DATABASE",
9612            Self::Role => "ROLE",
9613            Self::Tablespace => "TABLESPACE",
9614        })
9615    }
9616}
9617
9618impl<X: Extension + Render> Render for AggregateArgs<X> {
9619    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9620        match self {
9621            Self::Star { .. } => f.write_str("(*)"),
9622            Self::Types {
9623                direct, order_by, ..
9624            } => {
9625                f.write_str("(")?;
9626                render_comma_separated(direct, ctx, f)?;
9627                if let Some(order_by) = order_by {
9628                    if !direct.is_empty() {
9629                        f.write_str(" ")?;
9630                    }
9631                    f.write_str("ORDER BY ")?;
9632                    render_comma_separated(order_by, ctx, f)?;
9633                }
9634                f.write_str(")")
9635            }
9636        }
9637    }
9638}
9639
9640impl<X: Extension + Render> Render for OperatorArgs<X> {
9641    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9642        f.write_str("(")?;
9643        match &self.left {
9644            Some(left) => left.render(ctx, f)?,
9645            None => f.write_str("NONE")?,
9646        }
9647        f.write_str(", ")?;
9648        match &self.right {
9649            Some(right) => right.render(ctx, f)?,
9650            None => f.write_str("NONE")?,
9651        }
9652        f.write_str(")")
9653    }
9654}
9655
9656impl<X: Extension + Render> Render for CreateTypeDefinition<X> {
9657    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9658        match self {
9659            // Reuses the `ENUM(...)` value-list spelling of `DataType::Enum`; an empty label
9660            // list renders `ENUM()` (which DuckDB accepts).
9661            Self::Enum { labels, .. } => render_value_list_type(ctx, "ENUM", labels, f),
9662            Self::EnumFromQuery { query, .. } => {
9663                f.write_str("ENUM (")?;
9664                query.render(ctx, f)?;
9665                f.write_str(")")
9666            }
9667            Self::Alias { data_type, .. } => data_type.render(ctx, f),
9668        }
9669    }
9670}
9671
9672impl Render for ViewCheckOption {
9673    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9674        f.write_str(match self {
9675            Self::Unspecified => "WITH CHECK OPTION",
9676            Self::Cascaded => "WITH CASCADED CHECK OPTION",
9677            Self::Local => "WITH LOCAL CHECK OPTION",
9678        })
9679    }
9680}
9681
9682impl<X: Extension + Render> Render for CreateIndex<X> {
9683    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9684        f.write_str("CREATE")?;
9685        if self.unique {
9686            f.write_str(" UNIQUE")?;
9687        }
9688        f.write_str(" INDEX")?;
9689        if self.concurrently {
9690            f.write_str(" CONCURRENTLY")?;
9691        }
9692        if self.if_not_exists {
9693            f.write_str(" IF NOT EXISTS")?;
9694        }
9695        if let Some(name) = &self.name {
9696            f.write_str(" ")?;
9697            name.render(ctx, f)?;
9698        }
9699        f.write_str(" ON ")?;
9700        self.table.render(ctx, f)?;
9701        if let Some(using) = &self.using {
9702            f.write_str(" USING ")?;
9703            using.render(ctx, f)?;
9704        }
9705        f.write_str(" (")?;
9706        render_comma_separated(&self.columns, ctx, f)?;
9707        f.write_str(")")?;
9708        if let Some(predicate) = &self.predicate {
9709            f.write_str(" WHERE ")?;
9710            predicate.render(ctx, f)?;
9711        }
9712        Ok(())
9713    }
9714}
9715
9716/// Render the trailing ` ASC`/` DESC` then ` NULLS FIRST`/` NULLS LAST` sort
9717/// suffixes of a sort key — shared by `IndexColumn` and `OrderByExpr`. Each is a
9718/// leading-space clause omitted when its option is `None`.
9719fn render_sort_direction(
9720    asc: Option<bool>,
9721    nulls_first: Option<bool>,
9722    f: &mut fmt::Formatter<'_>,
9723) -> fmt::Result {
9724    match asc {
9725        Some(true) => f.write_str(" ASC")?,
9726        Some(false) => f.write_str(" DESC")?,
9727        None => {}
9728    }
9729    match nulls_first {
9730        Some(true) => f.write_str(" NULLS FIRST")?,
9731        Some(false) => f.write_str(" NULLS LAST")?,
9732        None => {}
9733    }
9734    Ok(())
9735}
9736
9737impl<X: Extension + Render> Render for IndexColumn<X> {
9738    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9739        self.expr.render(ctx, f)?;
9740        render_sort_direction(self.asc, self.nulls_first, f)
9741    }
9742}
9743
9744impl Render for CreateDatabase {
9745    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9746        f.write_str("CREATE DATABASE ")?;
9747        if self.if_not_exists {
9748            f.write_str("IF NOT EXISTS ")?;
9749        }
9750        self.name.render(ctx, f)
9751    }
9752}
9753
9754impl<X: Extension + Render> Render for CreateFunction<X> {
9755    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9756        f.write_str("CREATE ")?;
9757        if self.or_replace {
9758            f.write_str("OR REPLACE ")?;
9759        }
9760        if let Some(definer) = &self.definer {
9761            definer.render(ctx, f)?;
9762            f.write_str(" ")?;
9763        }
9764        f.write_str("FUNCTION ")?;
9765        if self.if_not_exists {
9766            f.write_str("IF NOT EXISTS ")?;
9767        }
9768        self.name.render(ctx, f)?;
9769        // The parameter list is always parenthesized, even when empty.
9770        f.write_str("(")?;
9771        render_comma_separated(&self.params, ctx, f)?;
9772        f.write_str(")")?;
9773        if let Some(returns) = &self.returns {
9774            f.write_str(" RETURNS ")?;
9775            returns.render(ctx, f)?;
9776        }
9777        for option in &self.options {
9778            f.write_str(" ")?;
9779            option.render(ctx, f)?;
9780        }
9781        // The trailing SQL-standard routine body (`RETURN <expr>`) follows the whole option
9782        // list, its written source position (proven by oracle: it never precedes an option).
9783        if let Some(body) = &self.body {
9784            f.write_str(" ")?;
9785            body.render(ctx, f)?;
9786        }
9787        Ok(())
9788    }
9789}
9790
9791impl<X: Extension + Render> Render for FunctionParam<X> {
9792    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9793        if let Some(mode) = &self.mode {
9794            f.write_str(match mode {
9795                FunctionParamMode::In => "IN ",
9796                FunctionParamMode::Out => "OUT ",
9797                FunctionParamMode::InOut => "INOUT ",
9798                FunctionParamMode::Variadic => "VARIADIC ",
9799            })?;
9800        }
9801        if let Some(name) = &self.name {
9802            name.render(ctx, f)?;
9803            f.write_str(" ")?;
9804        }
9805        self.data_type.render(ctx, f)?;
9806        if let Some(default) = &self.default {
9807            f.write_str(" ")?;
9808            default.render(ctx, f)?;
9809        }
9810        Ok(())
9811    }
9812}
9813
9814impl<X: Extension + Render> Render for FunctionParamDefault<X> {
9815    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9816        f.write_str(match self.spelling {
9817            FunctionParamDefaultSpelling::Default => "DEFAULT ",
9818            FunctionParamDefaultSpelling::Equals => "= ",
9819        })?;
9820        self.value.render(ctx, f)
9821    }
9822}
9823
9824impl<X: Extension + Render> Render for CreateMacro<X> {
9825    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9826        f.write_str("CREATE")?;
9827        if self.or_replace {
9828            f.write_str(" OR REPLACE")?;
9829        }
9830        if let Some(temporary) = self.temporary {
9831            f.write_str(" ")?;
9832            temporary.render(ctx, f)?;
9833        }
9834        f.write_str(match self.spelling {
9835            MacroSpelling::Macro => " MACRO",
9836            MacroSpelling::Function => " FUNCTION",
9837        })?;
9838        if self.if_not_exists {
9839            f.write_str(" IF NOT EXISTS")?;
9840        }
9841        f.write_str(" ")?;
9842        self.name.render(ctx, f)?;
9843        // The parameter list is always parenthesized, even when empty.
9844        f.write_str("(")?;
9845        render_comma_separated(&self.params, ctx, f)?;
9846        f.write_str(") AS ")?;
9847        self.body.render(ctx, f)
9848    }
9849}
9850
9851impl<X: Extension + Render> Render for MacroParam<X> {
9852    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9853        self.name.render(ctx, f)?;
9854        if let Some(default) = &self.default {
9855            f.write_str(" := ")?;
9856            default.render(ctx, f)?;
9857        }
9858        Ok(())
9859    }
9860}
9861
9862impl<X: Extension + Render> Render for MacroBody<X> {
9863    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9864        match self {
9865            Self::Scalar { expr, .. } => expr.render(ctx, f),
9866            Self::Table { query, .. } => {
9867                f.write_str("TABLE ")?;
9868                query.render(ctx, f)
9869            }
9870        }
9871    }
9872}
9873
9874impl<X: Extension + Render> Render for FunctionOption<X> {
9875    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9876        match self {
9877            Self::Language { name, .. } => {
9878                f.write_str("LANGUAGE ")?;
9879                name.render(ctx, f)
9880            }
9881            Self::As { body, .. } => {
9882                f.write_str("AS ")?;
9883                body.render(ctx, f)
9884            }
9885            Self::NullBehavior { behavior, .. } => behavior.render(ctx, f),
9886            Self::Deterministic { not, .. } => f.write_str(if *not {
9887                "NOT DETERMINISTIC"
9888            } else {
9889                "DETERMINISTIC"
9890            }),
9891            Self::DataAccess { access, .. } => access.render(ctx, f),
9892            Self::SqlSecurity { context, .. } => {
9893                f.write_str("SQL SECURITY ")?;
9894                context.render(ctx, f)
9895            }
9896            Self::Comment { comment, .. } => {
9897                f.write_str("COMMENT ")?;
9898                comment.render(ctx, f)
9899            }
9900        }
9901    }
9902}
9903
9904impl Render for SqlDataAccess {
9905    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9906        f.write_str(match self {
9907            Self::ContainsSql => "CONTAINS SQL",
9908            Self::NoSql => "NO SQL",
9909            Self::ReadsSqlData => "READS SQL DATA",
9910            Self::ModifiesSqlData => "MODIFIES SQL DATA",
9911        })
9912    }
9913}
9914
9915impl Render for SqlSecurityContext {
9916    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9917        f.write_str(match self {
9918            Self::Definer => "DEFINER",
9919            Self::Invoker => "INVOKER",
9920        })
9921    }
9922}
9923
9924impl<X: Extension + Render> Render for FunctionBody<X> {
9925    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9926        match self {
9927            // The body [`Literal`] renders from its source span, so a dollar-quoted body
9928            // (`$tag$…$tag$`) reproduces its delimiters and verbatim text exactly.
9929            Self::Definition { definition, .. } => definition.render(ctx, f),
9930            // The SQL-standard live body: the `RETURN` keyword then the expression.
9931            Self::Return { expr, .. } => {
9932                f.write_str("RETURN ")?;
9933                expr.render(ctx, f)
9934            }
9935            // The MySQL SQL/PSM body statement (usually a `BEGIN … END` compound block).
9936            Self::Block { body, .. } => body.render(ctx, f),
9937        }
9938    }
9939}
9940
9941impl Render for Definer {
9942    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9943        f.write_str("DEFINER = ")?;
9944        match self {
9945            Self::Account { user, host, .. } => {
9946                user.render(ctx, f)?;
9947                if let Some(host) = host {
9948                    f.write_str("@")?;
9949                    host.render(ctx, f)?;
9950                }
9951                Ok(())
9952            }
9953            Self::CurrentUser { parens, .. } => {
9954                f.write_str("CURRENT_USER")?;
9955                if *parens {
9956                    f.write_str("()")?;
9957                }
9958                Ok(())
9959            }
9960        }
9961    }
9962}
9963
9964impl<X: Extension + Render> Render for CreateProcedure<X> {
9965    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9966        f.write_str("CREATE ")?;
9967        if let Some(definer) = &self.definer {
9968            definer.render(ctx, f)?;
9969            f.write_str(" ")?;
9970        }
9971        f.write_str("PROCEDURE ")?;
9972        if self.if_not_exists {
9973            f.write_str("IF NOT EXISTS ")?;
9974        }
9975        self.name.render(ctx, f)?;
9976        // The parameter list is always parenthesized, even when empty.
9977        f.write_str("(")?;
9978        render_comma_separated(&self.params, ctx, f)?;
9979        f.write_str(")")?;
9980        for characteristic in &self.characteristics {
9981            f.write_str(" ")?;
9982            characteristic.render(ctx, f)?;
9983        }
9984        f.write_str(" ")?;
9985        self.body.render(ctx, f)
9986    }
9987}
9988
9989impl<X: Extension + Render> Render for AlterRoutine<X> {
9990    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9991        f.write_str(match self.kind {
9992            RoutineKind::Procedure => "ALTER PROCEDURE ",
9993            RoutineKind::Function => "ALTER FUNCTION ",
9994        })?;
9995        self.name.render(ctx, f)?;
9996        for characteristic in &self.characteristics {
9997            f.write_str(" ")?;
9998            characteristic.render(ctx, f)?;
9999        }
10000        Ok(())
10001    }
10002}
10003
10004impl Render for FunctionNullBehavior {
10005    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10006        f.write_str(match self {
10007            Self::CalledOnNull => "CALLED ON NULL INPUT",
10008            Self::ReturnsNullOnNull => "RETURNS NULL ON NULL INPUT",
10009            Self::Strict => "STRICT",
10010        })
10011    }
10012}
10013
10014impl<X: Extension + Render> Render for EventSchedule<X> {
10015    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10016        match self {
10017            Self::At { at, .. } => {
10018                f.write_str("AT ")?;
10019                at.render(ctx, f)
10020            }
10021            Self::Every {
10022                value,
10023                unit,
10024                starts,
10025                ends,
10026                ..
10027            } => {
10028                f.write_str("EVERY ")?;
10029                value.render(ctx, f)?;
10030                // The unit reuses the shared IntervalFields vocabulary in MySQL underscore
10031                // spelling; the suffix carries its own leading space.
10032                f.write_str(mysql_interval_unit_suffix(*unit))?;
10033                if let Some(starts) = starts {
10034                    f.write_str(" STARTS ")?;
10035                    starts.render(ctx, f)?;
10036                }
10037                if let Some(ends) = ends {
10038                    f.write_str(" ENDS ")?;
10039                    ends.render(ctx, f)?;
10040                }
10041                Ok(())
10042            }
10043        }
10044    }
10045}
10046
10047/// Render the ` ON COMPLETION [NOT] PRESERVE` clause (leading space) shared by
10048/// `CREATE`/`ALTER EVENT`.
10049fn render_event_on_completion(
10050    on_completion: EventOnCompletion,
10051    f: &mut fmt::Formatter<'_>,
10052) -> fmt::Result {
10053    f.write_str(match on_completion {
10054        EventOnCompletion::Preserve => " ON COMPLETION PRESERVE",
10055        EventOnCompletion::NotPreserve => " ON COMPLETION NOT PRESERVE",
10056    })
10057}
10058
10059/// Render the ` ENABLE | DISABLE [ON SLAVE|REPLICA]` status clause (leading space).
10060fn render_event_status(status: EventStatus, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10061    f.write_str(match status {
10062        EventStatus::Enable => " ENABLE",
10063        EventStatus::Disable => " DISABLE",
10064        EventStatus::DisableOnReplica(ReplicaSpelling::Slave) => " DISABLE ON SLAVE",
10065        EventStatus::DisableOnReplica(ReplicaSpelling::Replica) => " DISABLE ON REPLICA",
10066    })
10067}
10068
10069impl<X: Extension + Render> Render for CreateEvent<X> {
10070    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10071        f.write_str("CREATE ")?;
10072        if let Some(definer) = &self.definer {
10073            definer.render(ctx, f)?;
10074            f.write_str(" ")?;
10075        }
10076        f.write_str("EVENT ")?;
10077        if self.if_not_exists {
10078            f.write_str("IF NOT EXISTS ")?;
10079        }
10080        self.name.render(ctx, f)?;
10081        f.write_str(" ON SCHEDULE ")?;
10082        self.schedule.render(ctx, f)?;
10083        if let Some(on_completion) = self.on_completion {
10084            render_event_on_completion(on_completion, f)?;
10085        }
10086        if let Some(status) = self.status {
10087            render_event_status(status, f)?;
10088        }
10089        if let Some(comment) = &self.comment {
10090            f.write_str(" COMMENT ")?;
10091            comment.render(ctx, f)?;
10092        }
10093        f.write_str(" DO ")?;
10094        self.body.render(ctx, f)
10095    }
10096}
10097
10098impl<X: Extension + Render> Render for AlterEvent<X> {
10099    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10100        f.write_str("ALTER ")?;
10101        if let Some(definer) = &self.definer {
10102            definer.render(ctx, f)?;
10103            f.write_str(" ")?;
10104        }
10105        f.write_str("EVENT ")?;
10106        self.name.render(ctx, f)?;
10107        if let Some(schedule) = &self.schedule {
10108            f.write_str(" ON SCHEDULE ")?;
10109            schedule.render(ctx, f)?;
10110        }
10111        if let Some(on_completion) = self.on_completion {
10112            render_event_on_completion(on_completion, f)?;
10113        }
10114        if let Some(rename_to) = &self.rename_to {
10115            f.write_str(" RENAME TO ")?;
10116            rename_to.render(ctx, f)?;
10117        }
10118        if let Some(status) = self.status {
10119            render_event_status(status, f)?;
10120        }
10121        if let Some(comment) = &self.comment {
10122            f.write_str(" COMMENT ")?;
10123            comment.render(ctx, f)?;
10124        }
10125        if let Some(body) = &self.body {
10126            f.write_str(" DO ")?;
10127            body.render(ctx, f)?;
10128        }
10129        Ok(())
10130    }
10131}
10132
10133impl Render for DropEvent {
10134    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10135        f.write_str("DROP EVENT ")?;
10136        if self.if_exists {
10137            f.write_str("IF EXISTS ")?;
10138        }
10139        self.name.render(ctx, f)
10140    }
10141}
10142
10143impl Render for DropDatabase {
10144    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10145        f.write_str(match self.spelling {
10146            DatabaseKeyword::Database => "DROP DATABASE ",
10147            DatabaseKeyword::Schema => "DROP SCHEMA ",
10148        })?;
10149        if self.if_exists {
10150            f.write_str("IF EXISTS ")?;
10151        }
10152        self.name.render(ctx, f)
10153    }
10154}
10155
10156impl Render for DropIndexOnTable {
10157    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10158        f.write_str("DROP INDEX ")?;
10159        self.name.render(ctx, f)?;
10160        f.write_str(" ON ")?;
10161        self.table.render(ctx, f)?;
10162        for option in &self.options {
10163            f.write_str(" ")?;
10164            option.render(ctx, f)?;
10165        }
10166        Ok(())
10167    }
10168}
10169
10170impl Render for IndexLockAlgorithmOption {
10171    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10172        let (keyword, equals, value) = match self {
10173            Self::Algorithm { equals, value } => ("ALGORITHM", *equals, value.render_str()),
10174            Self::Lock { equals, value } => ("LOCK", *equals, value.render_str()),
10175        };
10176        f.write_str(keyword)?;
10177        f.write_str(if equals { " = " } else { " " })?;
10178        f.write_str(value)
10179    }
10180}
10181
10182impl IndexAlgorithm {
10183    /// The canonical keyword spelling of this algorithm value.
10184    fn render_str(self) -> &'static str {
10185        match self {
10186            Self::Default => "DEFAULT",
10187            Self::Inplace => "INPLACE",
10188            Self::Instant => "INSTANT",
10189            Self::Copy => "COPY",
10190        }
10191    }
10192}
10193
10194impl IndexLock {
10195    /// The canonical keyword spelling of this lock value.
10196    fn render_str(self) -> &'static str {
10197        match self {
10198            Self::Default => "DEFAULT",
10199            Self::None => "NONE",
10200            Self::Shared => "SHARED",
10201            Self::Exclusive => "EXCLUSIVE",
10202        }
10203    }
10204}
10205
10206impl<X: Extension + Render> Render for Query<X> {
10207    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10208        if let Some(with) = &self.with {
10209            with.render(ctx, f)?;
10210            f.write_str(" ")?;
10211        }
10212        self.body.render(ctx, f)?;
10213        if !self.order_by.is_empty() {
10214            f.write_str(" ORDER BY ")?;
10215            render_comma_separated(&self.order_by, ctx, f)?;
10216        }
10217        // DuckDB's `ORDER BY ALL` mode; the parser never populates it alongside
10218        // `order_by` (the engine rejects mixing), so the two arms cannot both fire.
10219        if let Some(all) = &self.order_by_all {
10220            f.write_str(" ORDER BY ")?;
10221            all.render(ctx, f)?;
10222        }
10223        // ClickHouse `LIMIT n [OFFSET m] BY …` precedes the ordinary `LIMIT`; a query
10224        // may carry both, so this is emitted before, not instead of, the `limit` tail.
10225        if let Some(limit_by) = &self.limit_by {
10226            f.write_str(" ")?;
10227            limit_by.render(ctx, f)?;
10228        }
10229        if let Some(limit) = &self.limit {
10230            f.write_str(" ")?;
10231            limit.render(ctx, f)?;
10232        }
10233        // ClickHouse `SETTINGS name = value, …` follows the ordinary `LIMIT` tail.
10234        if !self.settings.is_empty() {
10235            f.write_str(" SETTINGS ")?;
10236            render_comma_separated(&self.settings, ctx, f)?;
10237        }
10238        // ClickHouse `FORMAT <name>` closes the query, the last tail after `SETTINGS`.
10239        if let Some(format) = &self.format {
10240            f.write_str(" ")?;
10241            format.render(ctx, f)?;
10242        }
10243        // Row-locking clauses trail the whole query, after LIMIT (MySQL's fixed
10244        // position; PostgreSQL also accepts them here). Space-separated when stacked.
10245        for locking in &self.locking {
10246            f.write_str(" ")?;
10247            locking.render(ctx, f)?;
10248        }
10249        // BigQuery/ZetaSQL `|>` pipe operators trail everything else, one ` |> …` step per
10250        // element in written order. Empty for every shipped preset (the gate is off).
10251        for op in &self.pipe_operators {
10252            f.write_str(" ")?;
10253            op.render(ctx, f)?;
10254        }
10255        // MSSQL `FOR XML`/`FOR JSON` result-shaping tail closes the query, after every
10256        // other clause. `None` unless the MSSQL/Lenient gate is on.
10257        if let Some(for_clause) = &self.for_clause {
10258            f.write_str(" ")?;
10259            for_clause.render(ctx, f)?;
10260        }
10261        Ok(())
10262    }
10263}
10264
10265impl<X: Extension + Render> Render for PipeOperator<X> {
10266    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10267        // The framework seam (render side): one arm per operator, each emitting
10268        // `|> <KEYWORD> …`. The leading ` ` and the whole-list loop live on `Query`'s
10269        // render, so an arm writes only its own `|> …`.
10270        match self {
10271            PipeOperator::Where { predicate, .. } => {
10272                f.write_str("|> WHERE ")?;
10273                predicate.render(ctx, f)
10274            }
10275            PipeOperator::Select { items, .. } => {
10276                f.write_str("|> SELECT ")?;
10277                render_comma_separated(items, ctx, f)
10278            }
10279            PipeOperator::Extend { items, .. } => {
10280                f.write_str("|> EXTEND ")?;
10281                render_comma_separated(items, ctx, f)
10282            }
10283            PipeOperator::As { alias, .. } => {
10284                f.write_str("|> AS ")?;
10285                alias.render(ctx, f)
10286            }
10287            PipeOperator::OrderBy { keys, .. } => {
10288                f.write_str("|> ORDER BY ")?;
10289                render_comma_separated(keys, ctx, f)
10290            }
10291            PipeOperator::Limit { count, offset, .. } => {
10292                f.write_str("|> LIMIT ")?;
10293                count.render(ctx, f)?;
10294                if let Some(offset) = offset {
10295                    f.write_str(" OFFSET ")?;
10296                    offset.render(ctx, f)?;
10297                }
10298                Ok(())
10299            }
10300            PipeOperator::Join { join, .. } => {
10301                // `Join` renders `<keyword> <relation> <constraint>`, so `|> ` + the join is
10302                // `|> [<type>] JOIN <relation> [ON | USING]`.
10303                f.write_str("|> ")?;
10304                join.render(ctx, f)
10305            }
10306            PipeOperator::SetOperation {
10307                op,
10308                quantifier,
10309                queries,
10310                ..
10311            } => {
10312                f.write_str("|> ")?;
10313                op.render(ctx, f)?;
10314                if let Some(quantifier) = quantifier {
10315                    f.write_str(" ")?;
10316                    quantifier.render(ctx, f)?;
10317                }
10318                f.write_str(" ")?;
10319                // Each operand is a parenthesized query; `Query`'s render emits no
10320                // surrounding parentheses, so they are written here.
10321                for (index, query) in queries.iter().enumerate() {
10322                    if index > 0 {
10323                        f.write_str(", ")?;
10324                    }
10325                    f.write_str("(")?;
10326                    query.render(ctx, f)?;
10327                    f.write_str(")")?;
10328                }
10329                Ok(())
10330            }
10331            PipeOperator::Set { assignments, .. } => {
10332                f.write_str("|> SET ")?;
10333                render_comma_separated(assignments, ctx, f)
10334            }
10335            PipeOperator::Call { call, alias, .. } => {
10336                f.write_str("|> CALL ")?;
10337                call.render(ctx, f)?;
10338                if let Some(alias) = alias {
10339                    f.write_str(" AS ")?;
10340                    alias.render(ctx, f)?;
10341                }
10342                Ok(())
10343            }
10344            PipeOperator::Aggregate {
10345                aggregates,
10346                group_by,
10347                ..
10348            } => {
10349                // The aggregate list is empty for a grouping-only operator, so the
10350                // separating space is written only when there is a list to follow it.
10351                f.write_str("|> AGGREGATE")?;
10352                if !aggregates.is_empty() {
10353                    f.write_str(" ")?;
10354                    render_comma_separated(aggregates, ctx, f)?;
10355                }
10356                if !group_by.is_empty() {
10357                    f.write_str(" GROUP BY ")?;
10358                    render_comma_separated(group_by, ctx, f)?;
10359                }
10360                Ok(())
10361            }
10362            PipeOperator::Drop { columns, .. } => {
10363                f.write_str("|> DROP ")?;
10364                render_comma_separated(columns, ctx, f)
10365            }
10366            PipeOperator::Rename { renames, .. } => {
10367                f.write_str("|> RENAME ")?;
10368                render_comma_separated(renames, ctx, f)
10369            }
10370            PipeOperator::Pivot {
10371                aggregates, column, ..
10372            } => {
10373                f.write_str("|> PIVOT (")?;
10374                render_comma_separated(aggregates, ctx, f)?;
10375                f.write_str(" FOR ")?;
10376                // `PivotColumn` renders `<col> IN (<values>)`.
10377                column.render(ctx, f)?;
10378                f.write_str(")")
10379            }
10380            PipeOperator::Unpivot {
10381                value,
10382                name,
10383                columns,
10384                ..
10385            } => {
10386                f.write_str("|> UNPIVOT (")?;
10387                render_unpivot_name_list(value, ctx, f)?;
10388                f.write_str(" FOR ")?;
10389                render_unpivot_name_list(name, ctx, f)?;
10390                f.write_str(" IN (")?;
10391                render_comma_separated(columns, ctx, f)?;
10392                f.write_str("))")
10393            }
10394            PipeOperator::TableSample { sample, .. } => {
10395                // `TableSample`'s render carries its own leading ` TABLESAMPLE ` (it is a
10396                // `FROM`-suffix node), so the `|>` is written bare against it.
10397                f.write_str("|>")?;
10398                sample.render(ctx, f)
10399            }
10400        }
10401    }
10402}
10403
10404impl<X: Extension + Render> Render for SemiStructuredAccessExpr<X> {
10405    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10406        let full = ctx.mode() == RenderMode::Parenthesized;
10407        open_group(full, f)?;
10408        render_operand(
10409            &self.base,
10410            operand_needs_parens(
10411                &ctx.target().binding_powers,
10412                ctx.target().binding_powers.subscript,
10413                &self.base,
10414                Side::Left,
10415            ),
10416            ctx,
10417            f,
10418        )?;
10419        let mut segments = self.path.iter();
10420        if let Some(first) = segments.next() {
10421            f.write_str(":")?;
10422            render_semi_structured_path_segment(first, false, ctx, f)?;
10423        }
10424        for segment in segments {
10425            render_semi_structured_path_segment(segment, true, ctx, f)?;
10426        }
10427        close_group(full, f)
10428    }
10429}
10430
10431fn render_semi_structured_path_segment<X: Extension + Render>(
10432    segment: &SemiStructuredPathSegment<X>,
10433    suffix: bool,
10434    ctx: &RenderCtx<'_>,
10435    f: &mut fmt::Formatter<'_>,
10436) -> fmt::Result {
10437    match segment {
10438        SemiStructuredPathSegment::Key { key, .. } => {
10439            if suffix {
10440                f.write_str(".")?;
10441            }
10442            key.render(ctx, f)
10443        }
10444        SemiStructuredPathSegment::Index { index, .. } => {
10445            f.write_str("[")?;
10446            index.render(ctx, f)?;
10447            f.write_str("]")
10448        }
10449    }
10450}
10451
10452impl<X: Extension + Render> Render for PipeAggregateExpr<X> {
10453    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10454        self.expr.render(ctx, f)?;
10455        if let Some(alias) = &self.alias {
10456            f.write_str(" AS ")?;
10457            alias.render(ctx, f)?;
10458        }
10459        render_sort_direction(self.asc, self.nulls_first, f)
10460    }
10461}
10462
10463impl Render for PipeRenameItem {
10464    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10465        self.old.render(ctx, f)?;
10466        f.write_str(" AS ")?;
10467        self.new.render(ctx, f)
10468    }
10469}
10470
10471impl Render for LockingClause {
10472    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10473        match self.spelling {
10474            // MySQL's legacy spelling of `FOR SHARE`, a bare clause with no `OF`/wait
10475            // tail (the parser only builds it on `LockStrength::Share`).
10476            LockingSpelling::LockInShareMode => return f.write_str("LOCK IN SHARE MODE"),
10477            LockingSpelling::Modern => {}
10478        }
10479        f.write_str(match self.strength {
10480            LockStrength::Update => "FOR UPDATE",
10481            LockStrength::NoKeyUpdate => "FOR NO KEY UPDATE",
10482            LockStrength::Share => "FOR SHARE",
10483            LockStrength::KeyShare => "FOR KEY SHARE",
10484        })?;
10485        if !self.of.is_empty() {
10486            f.write_str(" OF ")?;
10487            render_comma_separated(&self.of, ctx, f)?;
10488        }
10489        match self.wait {
10490            Some(LockWait::NoWait) => f.write_str(" NOWAIT")?,
10491            Some(LockWait::SkipLocked) => f.write_str(" SKIP LOCKED")?,
10492            None => {}
10493        }
10494        Ok(())
10495    }
10496}
10497
10498impl Render for IndexHint {
10499    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10500        f.write_str(match self.action {
10501            IndexHintAction::Use => "USE ",
10502            IndexHintAction::Ignore => "IGNORE ",
10503            IndexHintAction::Force => "FORCE ",
10504        })?;
10505        f.write_str(match self.keyword {
10506            IndexHintKeyword::Index => "INDEX",
10507            IndexHintKeyword::Key => "KEY",
10508        })?;
10509        if let Some(scope) = self.scope {
10510            f.write_str(match scope {
10511                IndexHintScope::Join => " FOR JOIN",
10512                IndexHintScope::OrderBy => " FOR ORDER BY",
10513                IndexHintScope::GroupBy => " FOR GROUP BY",
10514            })?;
10515        }
10516        // The parenthesized index list is mandatory syntax even when empty
10517        // (`USE INDEX ()`), so the parens are always written.
10518        f.write_str(" (")?;
10519        render_comma_separated(&self.indexes, ctx, f)?;
10520        f.write_str(")")
10521    }
10522}
10523
10524impl Render for IndexedBy {
10525    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10526        match self {
10527            IndexedBy::Named { index, .. } => {
10528                f.write_str("INDEXED BY ")?;
10529                index.render(ctx, f)
10530            }
10531            IndexedBy::NotIndexed { .. } => f.write_str("NOT INDEXED"),
10532        }
10533    }
10534}
10535
10536impl Render for TableHint {
10537    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10538        match self {
10539            TableHint::Keyword { keyword, .. } => f.write_str(keyword.as_str()),
10540            // `INDEX = <index>` for a single index under the `=` spelling; otherwise the
10541            // parenthesized list (`INDEX (a, b)` / `INDEX = (a, b)`).
10542            TableHint::Index {
10543                equals, indexes, ..
10544            } => {
10545                if *equals && indexes.len() == 1 {
10546                    f.write_str("INDEX = ")?;
10547                    indexes[0].render(ctx, f)
10548                } else {
10549                    f.write_str(if *equals { "INDEX = (" } else { "INDEX (" })?;
10550                    render_comma_separated(indexes, ctx, f)?;
10551                    f.write_str(")")
10552                }
10553            }
10554            TableHint::ForceSeek { target: None, .. } => f.write_str("FORCESEEK"),
10555            TableHint::ForceSeek {
10556                target: Some(target),
10557                ..
10558            } => {
10559                f.write_str("FORCESEEK (")?;
10560                target.index.render(ctx, f)?;
10561                f.write_str(" (")?;
10562                render_comma_separated(&target.columns, ctx, f)?;
10563                f.write_str("))")
10564            }
10565            TableHint::Other { ident, .. } => ident.render(ctx, f),
10566        }
10567    }
10568}
10569
10570impl<X: Extension + Render> Render for TableVersion<X> {
10571    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10572        match self {
10573            TableVersion::ForSystemTimeAsOf { point, .. } => {
10574                f.write_str("FOR SYSTEM_TIME AS OF ")?;
10575                point.render(ctx, f)
10576            }
10577            TableVersion::ForSystemTimeFromTo { start, end, .. } => {
10578                f.write_str("FOR SYSTEM_TIME FROM ")?;
10579                start.render(ctx, f)?;
10580                f.write_str(" TO ")?;
10581                end.render(ctx, f)
10582            }
10583            TableVersion::ForSystemTimeBetween { start, end, .. } => {
10584                f.write_str("FOR SYSTEM_TIME BETWEEN ")?;
10585                start.render(ctx, f)?;
10586                f.write_str(" AND ")?;
10587                end.render(ctx, f)
10588            }
10589            TableVersion::ForSystemTimeContainedIn { start, end, .. } => {
10590                f.write_str("FOR SYSTEM_TIME CONTAINED IN (")?;
10591                start.render(ctx, f)?;
10592                f.write_str(", ")?;
10593                end.render(ctx, f)?;
10594                f.write_str(")")
10595            }
10596            TableVersion::ForSystemTimeAll { .. } => f.write_str("FOR SYSTEM_TIME ALL"),
10597            TableVersion::VersionAsOf { version, .. } => {
10598                f.write_str("VERSION AS OF ")?;
10599                version.render(ctx, f)
10600            }
10601            TableVersion::TimestampAsOf { point, .. } => {
10602                f.write_str("TIMESTAMP AS OF ")?;
10603                point.render(ctx, f)
10604            }
10605        }
10606    }
10607}
10608
10609impl<X: Extension + Render> Render for SetExpr<X> {
10610    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10611        match self {
10612            SetExpr::Select { select, .. } => select.render(ctx, f),
10613            SetExpr::Values { values, .. } => values.render(ctx, f),
10614            // A nested query body carries its own ORDER BY / LIMIT, so it is
10615            // parenthesized to stay unambiguous inside an enclosing query.
10616            SetExpr::Query { query, .. } => {
10617                f.write_str("(")?;
10618                query.render(ctx, f)?;
10619                f.write_str(")")
10620            }
10621            SetExpr::SetOperation {
10622                op,
10623                all,
10624                by_name,
10625                left,
10626                right,
10627                ..
10628            } => {
10629                render_set_operand(op, left, Side::Left, ctx, f)?;
10630                f.write_str(" ")?;
10631                op.render(ctx, f)?;
10632                if *all {
10633                    f.write_str(" ALL")?;
10634                }
10635                // DuckDB's name-matched `UNION [ALL] BY NAME`, written after the
10636                // optional `ALL` (`UNION ALL BY NAME`; probed on 1.5.4).
10637                if *by_name {
10638                    f.write_str(" BY NAME")?;
10639                }
10640                f.write_str(" ")?;
10641                render_set_operand(op, right, Side::Right, ctx, f)
10642            }
10643            // A statement-spelled PIVOT/UNPIVOT standing as a query body renders bare;
10644            // the enclosing position (a CTE's `( … )`, a `CREATE VIEW … AS`) supplies
10645            // any parentheses, exactly as `SetExpr::Select` does.
10646            SetExpr::Pivot { pivot, .. } => pivot.render(ctx, f),
10647            SetExpr::Unpivot { unpivot, .. } => unpivot.render(ctx, f),
10648        }
10649    }
10650}
10651
10652/// Render a child set expression under a set-operation parent.
10653///
10654/// In `Parenthesized` mode every nested `SetOperation` operand is wrapped; the
10655/// root query body stays unwrapped so a rendered statement still starts with its
10656/// query keyword. Canonical/redacted rendering adds only the parentheses needed
10657/// to preserve grouping under the target dialect's set-operation table.
10658fn render_set_operand<X: Extension + Render>(
10659    parent: &SetOperator,
10660    child: &SetExpr<X>,
10661    side: Side,
10662    ctx: &RenderCtx<'_>,
10663    f: &mut fmt::Formatter<'_>,
10664) -> fmt::Result {
10665    let wrap = match ctx.mode() {
10666        RenderMode::Parenthesized => matches!(child, SetExpr::SetOperation { .. }),
10667        RenderMode::Canonical | RenderMode::Redacted => {
10668            set_child_needs_parens(&ctx.target().set_operation_powers, parent, child, side)
10669        }
10670    };
10671    open_group(wrap, f)?;
10672    child.render(ctx, f)?;
10673    close_group(wrap, f)
10674}
10675
10676/// Whether a set-operation parent's child needs canonical parentheses.
10677fn set_child_needs_parens<X: Extension>(
10678    bp: &SetOperationBindingPowerTable,
10679    parent: &SetOperator,
10680    child: &SetExpr<X>,
10681    side: Side,
10682) -> bool {
10683    match child {
10684        SetExpr::SetOperation { op, .. } => bp.needs_parens(parent, op, side),
10685        _ => false,
10686    }
10687}
10688
10689impl<X: Extension + Render> Render for With<X> {
10690    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10691        f.write_str("WITH")?;
10692        if self.recursive {
10693            f.write_str(" RECURSIVE")?;
10694        }
10695        render_leading_space_comma_separated(&self.ctes, ctx, f)
10696    }
10697}
10698
10699impl<X: Extension + Render> Render for Cte<X> {
10700    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10701        self.name.render(ctx, f)?;
10702        if !self.columns.is_empty() {
10703            f.write_str("(")?;
10704            render_ident_list(&self.columns, ctx, f)?;
10705            f.write_str(")")?;
10706        }
10707        // DuckDB's `USING KEY (cols)` key clause sits between the column list and `AS`.
10708        if let Some(key_columns) = &self.using_key {
10709            f.write_str(" USING KEY (")?;
10710            render_ident_list(key_columns, ctx, f)?;
10711            f.write_str(")")?;
10712        }
10713        f.write_str(" AS")?;
10714        match self.materialized {
10715            Some(true) => f.write_str(" MATERIALIZED")?,
10716            Some(false) => f.write_str(" NOT MATERIALIZED")?,
10717            None => {}
10718        }
10719        f.write_str(" (")?;
10720        self.body.render(ctx, f)?;
10721        f.write_str(")")?;
10722        // The SQL:2023 recursive-query clauses trail the body's `)`, SEARCH before CYCLE.
10723        if let Some(search) = &self.search {
10724            f.write_str(" ")?;
10725            search.render(ctx, f)?;
10726        }
10727        if let Some(cycle) = &self.cycle {
10728            f.write_str(" ")?;
10729            cycle.render(ctx, f)?;
10730        }
10731        Ok(())
10732    }
10733}
10734
10735impl Render for CteSearchClause {
10736    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10737        f.write_str(if self.breadth_first {
10738            "SEARCH BREADTH FIRST BY "
10739        } else {
10740            "SEARCH DEPTH FIRST BY "
10741        })?;
10742        render_ident_list(&self.columns, ctx, f)?;
10743        f.write_str(" SET ")?;
10744        self.set_column.render(ctx, f)
10745    }
10746}
10747
10748impl<X: Extension + Render> Render for CteCycleClause<X> {
10749    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10750        f.write_str("CYCLE ")?;
10751        render_ident_list(&self.columns, ctx, f)?;
10752        f.write_str(" SET ")?;
10753        self.mark_column.render(ctx, f)?;
10754        // The `TO value DEFAULT default` mark pair, when present, renders its own leading
10755        // space; the short form emits nothing between the mark column and `USING`.
10756        if let Some(mark) = &self.mark {
10757            mark.render(ctx, f)?;
10758        }
10759        f.write_str(" USING ")?;
10760        self.path_column.render(ctx, f)
10761    }
10762}
10763
10764impl<X: Extension + Render> Render for CteCycleMark<X> {
10765    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10766        f.write_str(" TO ")?;
10767        self.value.render(ctx, f)?;
10768        f.write_str(" DEFAULT ")?;
10769        self.default.render(ctx, f)
10770    }
10771}
10772
10773impl<X: Extension + Render> Render for CteBody<X> {
10774    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10775        // Each DML arm's node renders its own leading `WITH` and `RETURNING` tail,
10776        // so a nested `WITH u AS (…) INSERT …` body round-trips through the arm.
10777        match self {
10778            Self::Query { query, .. } => query.render(ctx, f),
10779            Self::Insert { insert, .. } => insert.render(ctx, f),
10780            Self::Update { update, .. } => update.render(ctx, f),
10781            Self::Delete { delete, .. } => delete.render(ctx, f),
10782            Self::Merge { merge, .. } => merge.render(ctx, f),
10783        }
10784    }
10785}
10786
10787impl<X: Extension + Render> Render for Values<X> {
10788    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10789        f.write_str("VALUES ")?;
10790        render_values_rows(&self.rows, self.explicit_row, ctx, f)
10791    }
10792}
10793
10794impl<X: Extension + Render> Render for ValuesItem<X> {
10795    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10796        match self {
10797            Self::Expr { expr, .. } => expr.render(ctx, f),
10798            Self::Default { default, .. } => default.render(ctx, f),
10799        }
10800    }
10801}
10802
10803/// Render the `SELECT [DISTINCT …] [STRAIGHT_JOIN] <projection>` head shared by the
10804/// SELECT-first and FROM-first spellings. Writes the leading `SELECT`; any preceding
10805/// space is the caller's.
10806fn render_select_projection_clause<X: Extension + Render>(
10807    select: &Select<X>,
10808    ctx: &RenderCtx<'_>,
10809    f: &mut fmt::Formatter<'_>,
10810) -> fmt::Result {
10811    f.write_str("SELECT")?;
10812    match &select.distinct {
10813        None => {}
10814        Some(SelectDistinct::Quantifier { quantifier, .. }) => {
10815            f.write_str(" ")?;
10816            quantifier.render(ctx, f)?;
10817        }
10818        Some(SelectDistinct::On { exprs, .. }) => {
10819            f.write_str(" DISTINCT ON (")?;
10820            render_comma_separated(exprs, ctx, f)?;
10821            f.write_str(")")?;
10822        }
10823    }
10824    // MySQL `SELECT STRAIGHT_JOIN ...`, after the quantifier and before the list.
10825    if select.straight_join {
10826        f.write_str(" STRAIGHT_JOIN")?;
10827    }
10828    render_leading_space_comma_separated(&select.projection, ctx, f)
10829}
10830
10831/// Render the `[WHERE …] [GROUP BY … | GROUP BY ALL] [HAVING …] [WINDOW …] [QUALIFY …]`
10832/// tail shared by both spellings — every clause after the projection/`FROM` prefix, in
10833/// SQL order.
10834fn render_select_body_tail<X: Extension + Render>(
10835    select: &Select<X>,
10836    ctx: &RenderCtx<'_>,
10837    f: &mut fmt::Formatter<'_>,
10838) -> fmt::Result {
10839    // Hive/Spark lateral views trail the FROM clause and precede WHERE (the parser
10840    // only fills the field after a non-empty FROM).
10841    for view in &select.lateral_views {
10842        f.write_str(" ")?;
10843        view.render(ctx, f)?;
10844    }
10845    if let Some(selection) = &select.selection {
10846        f.write_str(" WHERE ")?;
10847        selection.render(ctx, f)?;
10848    }
10849    // The Oracle-style hierarchical query clause sits after WHERE and before GROUP BY;
10850    // the node renders its `START WITH`/`CONNECT BY` pair in the written order.
10851    if let Some(connect_by) = &select.connect_by {
10852        f.write_str(" ")?;
10853        connect_by.render(ctx, f)?;
10854    }
10855    if !select.group_by.is_empty() {
10856        f.write_str(" GROUP BY ")?;
10857        // PostgreSQL's grouping-set quantifier prefixes the item list (`GROUP BY DISTINCT
10858        // <items>`); the parser only sets it alongside a non-empty list, so it renders here.
10859        if let Some(quantifier) = &select.group_by_quantifier {
10860            quantifier.render(ctx, f)?;
10861            f.write_str(" ")?;
10862        }
10863        render_comma_separated(&select.group_by, ctx, f)?;
10864    }
10865    // DuckDB's `GROUP BY ALL` mode; the parser never sets it alongside a
10866    // non-empty `group_by` (the engine rejects mixing), so at most one arm fires.
10867    // The `*` shorthand round-trips only under a source-fidelity render; a
10868    // target-dialect re-spell and the redacted fingerprint canonicalize to `ALL`.
10869    if let Some(spelling) = select.group_by_all {
10870        let bare_star = spelling == GroupByAllSpelling::Star && honours_source_spelling(ctx);
10871        f.write_str(if bare_star {
10872            " GROUP BY *"
10873        } else {
10874            " GROUP BY ALL"
10875        })?;
10876    }
10877    if let Some(having) = &select.having {
10878        f.write_str(" HAVING ")?;
10879        having.render(ctx, f)?;
10880    }
10881    if !select.windows.is_empty() {
10882        f.write_str(" WINDOW ")?;
10883        render_comma_separated(&select.windows, ctx, f)?;
10884    }
10885    // QUALIFY follows the WINDOW clause — DuckDB's grammar order (`QUALIFY …
10886    // WINDOW …` is a DuckDB syntax error), so emitting it last round-trips.
10887    if let Some(qualify) = &select.qualify {
10888        f.write_str(" QUALIFY ")?;
10889        qualify.render(ctx, f)?;
10890    }
10891    // DuckDB's `USING SAMPLE` sample clause follows QUALIFY and precedes the enclosing
10892    // query's `ORDER BY` (the reverse order is a DuckDB syntax error).
10893    if let Some(sample) = &select.sample {
10894        f.write_str(" USING SAMPLE ")?;
10895        sample.render(ctx, f)?;
10896    }
10897    Ok(())
10898}
10899
10900/// True when a FROM-first body carries the implicit `SELECT *` of the bare
10901/// `FROM <tables>` form — a single unmodified wildcard with no `DISTINCT`. That surface
10902/// round-trips to `FROM <tables>` with the `SELECT *` left implicit (the one canonical
10903/// render for a FROM-first wildcard projection; an explicit `FROM t SELECT *` normalizes
10904/// onto it).
10905fn from_first_projection_is_implicit<X: Extension>(select: &Select<X>) -> bool {
10906    select.distinct.is_none()
10907        && matches!(select.projection.as_slice(), [SelectItem::Wildcard { .. }])
10908}
10909
10910impl<X: Extension + Render> Render for Select<X> {
10911    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10912        match self.spelling {
10913            // `TABLE name` round-trips its short-form spelling rather than expanding to
10914            // the canonical `SELECT * FROM name`. The parser is the only constructor of
10915            // this tag and guarantees the shape — a wildcard projection over one relation
10916            // — so the relation alone renders the whole command (ADR-0011).
10917            SelectSpelling::TableCommand => {
10918                f.write_str("TABLE ")?;
10919                if let Some(table) = self.from.first() {
10920                    table.relation.render(ctx, f)?;
10921                }
10922                Ok(())
10923            }
10924            // DuckDB's FROM-first order: the `FROM` clause leads, then the projection
10925            // (dropped when it is the bare `FROM t` implicit `SELECT *`), then the shared
10926            // tail. Every clause after the projection sits in its ordinary place.
10927            SelectSpelling::FromFirst => {
10928                f.write_str("FROM ")?;
10929                render_comma_separated(&self.from, ctx, f)?;
10930                if !from_first_projection_is_implicit(self) {
10931                    f.write_str(" ")?;
10932                    render_select_projection_clause(self, ctx, f)?;
10933                }
10934                render_select_body_tail(self, ctx, f)
10935            }
10936            SelectSpelling::Select => {
10937                render_select_projection_clause(self, ctx, f)?;
10938                // PostgreSQL `SELECT … INTO <table>` sits between the projection and `FROM`.
10939                if let Some(into) = &self.into {
10940                    f.write_str(" ")?;
10941                    into.render(ctx, f)?;
10942                }
10943                if !self.from.is_empty() {
10944                    f.write_str(" FROM ")?;
10945                    render_comma_separated(&self.from, ctx, f)?;
10946                }
10947                render_select_body_tail(self, ctx, f)
10948            }
10949        }
10950    }
10951}
10952
10953impl<X: Extension + Render> Render for LateralView<X> {
10954    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10955        f.write_str("LATERAL VIEW ")?;
10956        if self.outer {
10957            f.write_str("OUTER ")?;
10958        }
10959        self.function.render(ctx, f)?;
10960        f.write_str(" ")?;
10961        self.alias.render(ctx, f)?;
10962        if !self.columns.is_empty() {
10963            // The `AS` is canonical: the AS-less Spark spelling re-renders with the
10964            // keyword (a structural, not byte-exact, round-trip — see the node doc).
10965            f.write_str(" AS ")?;
10966            render_comma_separated(&self.columns, ctx, f)?;
10967        }
10968        Ok(())
10969    }
10970}
10971
10972impl<X: Extension + Render> Render for HierarchicalClause<X> {
10973    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10974        // `START WITH` and `CONNECT BY` render in the written order (Oracle admits
10975        // either); `NOCYCLE` always modifies `CONNECT BY`, whichever side it lands on.
10976        let render_start_with = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
10977            if let Some(start_with) = &self.start_with {
10978                f.write_str("START WITH ")?;
10979                start_with.render(ctx, f)?;
10980            }
10981            Ok(())
10982        };
10983        let render_connect_by = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
10984            f.write_str("CONNECT BY ")?;
10985            if self.nocycle {
10986                f.write_str("NOCYCLE ")?;
10987            }
10988            self.connect_by.render(ctx, f)
10989        };
10990        if self.start_with_leads && self.start_with.is_some() {
10991            render_start_with(f)?;
10992            f.write_str(" ")?;
10993            render_connect_by(f)
10994        } else if self.start_with.is_some() {
10995            render_connect_by(f)?;
10996            f.write_str(" ")?;
10997            render_start_with(f)
10998        } else {
10999            render_connect_by(f)
11000        }
11001    }
11002}
11003
11004impl Render for IntoTarget {
11005    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11006        f.write_str("INTO ")?;
11007        if let Some(temporary) = self.temporary {
11008            temporary.render(ctx, f)?;
11009            f.write_str(" ")?;
11010        }
11011        self.name.render(ctx, f)
11012    }
11013}
11014
11015impl<X: Extension + Render> Render for SelectItem<X> {
11016    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11017        match self {
11018            SelectItem::Wildcard {
11019                options,
11020                alias,
11021                alias_spelling,
11022                ..
11023            } => {
11024                f.write_str("*")?;
11025                render_optional_wildcard_options(options.as_deref(), ctx, f)?;
11026                render_alias(alias.as_ref(), *alias_spelling, ctx, f)
11027            }
11028            SelectItem::QualifiedWildcard {
11029                name,
11030                options,
11031                alias,
11032                alias_spelling,
11033                ..
11034            } => {
11035                name.render(ctx, f)?;
11036                f.write_str(".*")?;
11037                render_optional_wildcard_options(options.as_deref(), ctx, f)?;
11038                render_alias(alias.as_ref(), *alias_spelling, ctx, f)
11039            }
11040            SelectItem::Expr {
11041                expr,
11042                alias,
11043                alias_spelling,
11044                ..
11045            } => {
11046                // DuckDB's prefix form `alias: expr` writes the alias before the
11047                // value; a source-fidelity render reproduces it, a normalizing render
11048                // (`TargetDialect`/`Redacted`) falls through to the canonical trailing
11049                // `AS`.
11050                if let (Some(alias), AliasSpelling::PrefixColon) = (alias.as_ref(), alias_spelling)
11051                {
11052                    if honours_alias_spelling(ctx) {
11053                        alias.render(ctx, f)?;
11054                        f.write_str(": ")?;
11055                        return expr.render(ctx, f);
11056                    }
11057                }
11058                expr.render(ctx, f)?;
11059                if let Some(alias) = alias {
11060                    f.write_str(alias_lead(*alias_spelling, ctx))?;
11061                    alias.render(ctx, f)?;
11062                }
11063                Ok(())
11064            }
11065        }
11066    }
11067}
11068
11069/// Render the `EXCLUDE`/`REPLACE`/`RENAME` wildcard-modifier tail in DuckDB's fixed
11070/// canonical order, writing only the non-empty lists. Each list is parenthesized —
11071/// DuckDB's general spelling, which re-parses for the single-item bare form too (the
11072/// round-trip contract is structural, not byte-exact).
11073fn render_wildcard_options<X: Extension + Render>(
11074    options: &WildcardOptions<X>,
11075    ctx: &RenderCtx<'_>,
11076    f: &mut fmt::Formatter<'_>,
11077) -> fmt::Result {
11078    if !options.exclude.is_empty() {
11079        f.write_str(" EXCLUDE (")?;
11080        render_comma_separated(&options.exclude, ctx, f)?;
11081        f.write_str(")")?;
11082    }
11083    if !options.replace.is_empty() {
11084        f.write_str(" REPLACE (")?;
11085        render_comma_separated(&options.replace, ctx, f)?;
11086        f.write_str(")")?;
11087    }
11088    if !options.rename.is_empty() {
11089        f.write_str(" RENAME (")?;
11090        render_comma_separated(&options.rename, ctx, f)?;
11091        f.write_str(")")?;
11092    }
11093    Ok(())
11094}
11095
11096fn render_optional_wildcard_options<X: Extension + Render>(
11097    options: Option<&WildcardOptions<X>>,
11098    ctx: &RenderCtx<'_>,
11099    f: &mut fmt::Formatter<'_>,
11100) -> fmt::Result {
11101    match options {
11102        Some(options) => render_wildcard_options(options, ctx, f),
11103        None => Ok(()),
11104    }
11105}
11106
11107/// Render a list-comprehension source: a general expression, or the DuckDB column-star
11108/// `*` / `(* EXCLUDE (i))` form with its recorded parenthesization and wildcard modifiers.
11109fn render_comprehension_source<X: Extension + Render>(
11110    source: &ComprehensionSource<X>,
11111    ctx: &RenderCtx<'_>,
11112    f: &mut fmt::Formatter<'_>,
11113) -> fmt::Result {
11114    match source {
11115        ComprehensionSource::Expr { expr, .. } => expr.render(ctx, f),
11116        ComprehensionSource::Star {
11117            parenthesized,
11118            options,
11119            ..
11120        } => {
11121            if *parenthesized {
11122                f.write_str("(*")?;
11123                render_optional_wildcard_options(options.as_deref(), ctx, f)?;
11124                f.write_str(")")
11125            } else {
11126                f.write_str("*")?;
11127                render_optional_wildcard_options(options.as_deref(), ctx, f)
11128            }
11129        }
11130    }
11131}
11132
11133impl<X: Extension + Render> Render for WildcardReplace<X> {
11134    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11135        self.expr.render(ctx, f)?;
11136        f.write_str(" AS ")?;
11137        self.column.render(ctx, f)
11138    }
11139}
11140
11141impl Render for WildcardRename {
11142    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11143        self.column.render(ctx, f)?;
11144        f.write_str(" AS ")?;
11145        self.alias.render(ctx, f)
11146    }
11147}
11148
11149impl<X: Extension + Render> Render for GroupByItem<X> {
11150    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11151        match self {
11152            GroupByItem::Expr { expr, .. } => expr.render(ctx, f),
11153            GroupByItem::Rollup {
11154                exprs, spelling, ..
11155            } => match spelling {
11156                RollupSpelling::Function => {
11157                    f.write_str("ROLLUP (")?;
11158                    render_comma_separated(exprs, ctx, f)?;
11159                    f.write_str(")")
11160                }
11161                // MySQL's trailing modifier. The parser wraps the whole key list into
11162                // this single item, so it is the sole GROUP BY item — the GROUP BY
11163                // list's comma separator never fires between it and a sibling, so the
11164                // per-item renderer can emit the trailing form directly (no one-item
11165                // special case needed in the Select group_by rendering).
11166                RollupSpelling::WithRollup => {
11167                    render_comma_separated(exprs, ctx, f)?;
11168                    f.write_str(" WITH ROLLUP")
11169                }
11170            },
11171            GroupByItem::Cube { exprs, .. } => {
11172                f.write_str("CUBE (")?;
11173                render_comma_separated(exprs, ctx, f)?;
11174                f.write_str(")")
11175            }
11176            GroupByItem::GroupingSets { sets, .. } => {
11177                f.write_str("GROUPING SETS (")?;
11178                render_comma_separated(sets, ctx, f)?;
11179                f.write_str(")")
11180            }
11181            GroupByItem::Empty { .. } => f.write_str("()"),
11182        }
11183    }
11184}
11185
11186// ---------------------------------------------------------------------------
11187// FROM clause
11188// ---------------------------------------------------------------------------
11189
11190impl<X: Extension + Render> Render for TableWithJoins<X> {
11191    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11192        self.relation.render(ctx, f)?;
11193        for join in &self.joins {
11194            f.write_str(" ")?;
11195            join.render(ctx, f)?;
11196        }
11197        Ok(())
11198    }
11199}
11200
11201impl<X: Extension + Render> Render for TableFactor<X> {
11202    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11203        match self {
11204            TableFactor::Table {
11205                name,
11206                inheritance,
11207                json_path,
11208                version,
11209                partition,
11210                alias,
11211                indexed_by,
11212                index_hints,
11213                sample,
11214                table_hints,
11215                ..
11216            } => {
11217                render_relation_inheritance(inheritance, name, ctx, f)?;
11218                // A PartiQL / SUPER JSON path, attached directly to the table name with no
11219                // separating space (`FROM src[0].a`). The root is a bracket index; each
11220                // segment renders as its own suffix (`[index]` or `.key`).
11221                for segment in json_path {
11222                    render_semi_structured_path_segment(segment, true, ctx, f)?;
11223                }
11224                // A version / time-travel modifier, between the table name and the alias.
11225                if let Some(version) = version {
11226                    f.write_str(" ")?;
11227                    version.render(ctx, f)?;
11228                }
11229                // MySQL `PARTITION (p0, p1)`, between the table name and the alias.
11230                if !partition.is_empty() {
11231                    f.write_str(" PARTITION (")?;
11232                    render_comma_separated(partition, ctx, f)?;
11233                    f.write_str(")")?;
11234                }
11235                render_table_alias(alias.as_deref(), ctx, f)?;
11236                // SQLite `INDEXED BY <index>` / `NOT INDEXED`, immediately after the alias.
11237                if let Some(indexed_by) = indexed_by {
11238                    f.write_str(" ")?;
11239                    indexed_by.render(ctx, f)?;
11240                }
11241                // MySQL index hints, after the alias, space-separated (no comma).
11242                for hint in index_hints {
11243                    f.write_str(" ")?;
11244                    hint.render(ctx, f)?;
11245                }
11246                if let Some(sample) = sample {
11247                    sample.render(ctx, f)?;
11248                }
11249                // MSSQL `WITH (...)` table hints, after the tablesample clause,
11250                // comma-separated inside one parenthesized list.
11251                if !table_hints.is_empty() {
11252                    f.write_str(" WITH (")?;
11253                    render_comma_separated(table_hints, ctx, f)?;
11254                    f.write_str(")")?;
11255                }
11256                Ok(())
11257            }
11258            TableFactor::Derived {
11259                lateral,
11260                subquery,
11261                alias,
11262                spelling,
11263                ..
11264            } => {
11265                if *lateral {
11266                    f.write_str("LATERAL ")?;
11267                }
11268                match spelling {
11269                    DerivedSpelling::Parenthesized => {
11270                        f.write_str("(")?;
11271                        subquery.render(ctx, f)?;
11272                        f.write_str(")")?;
11273                    }
11274                    // DuckDB's bare `FROM VALUES (…) AS t`: the body (always a `VALUES`
11275                    // constructor) renders without the wrapping parentheses; the alias
11276                    // the parser required trails as usual via `render_table_alias`.
11277                    DerivedSpelling::BareValues => subquery.render(ctx, f)?,
11278                }
11279                render_table_alias(alias.as_deref(), ctx, f)
11280            }
11281            TableFactor::Function {
11282                lateral,
11283                function,
11284                with_ordinality,
11285                alias,
11286                column_defs,
11287                ..
11288            } => {
11289                if *lateral {
11290                    f.write_str("LATERAL ")?;
11291                }
11292                function.render(ctx, f)?;
11293                if *with_ordinality {
11294                    f.write_str(" WITH ORDINALITY")?;
11295                }
11296                render_function_alias(alias.as_deref(), column_defs, ctx, f)
11297            }
11298            TableFactor::RowsFrom {
11299                lateral,
11300                functions,
11301                with_ordinality,
11302                alias,
11303                ..
11304            } => {
11305                if *lateral {
11306                    f.write_str("LATERAL ")?;
11307                }
11308                f.write_str("ROWS FROM (")?;
11309                render_comma_separated(functions, ctx, f)?;
11310                f.write_str(")")?;
11311                if *with_ordinality {
11312                    f.write_str(" WITH ORDINALITY")?;
11313                }
11314                render_table_alias(alias.as_deref(), ctx, f)
11315            }
11316            TableFactor::Unnest {
11317                lateral,
11318                array_exprs,
11319                with_ordinality,
11320                alias,
11321                column_defs,
11322                with_offset,
11323                with_offset_alias,
11324                ..
11325            } => {
11326                if *lateral {
11327                    f.write_str("LATERAL ")?;
11328                }
11329                f.write_str("UNNEST(")?;
11330                render_comma_separated(array_exprs, ctx, f)?;
11331                f.write_str(")")?;
11332                // `WITH ORDINALITY` precedes the alias (PostgreSQL/DuckDB); `WITH OFFSET`
11333                // follows it (BigQuery). The two never co-occur, so rendering each at its
11334                // grammar position re-parses cleanly under whichever dialect produced it.
11335                if *with_ordinality {
11336                    f.write_str(" WITH ORDINALITY")?;
11337                }
11338                render_function_alias(alias.as_deref(), column_defs, ctx, f)?;
11339                if *with_offset {
11340                    f.write_str(" WITH OFFSET")?;
11341                    // The `WITH OFFSET` position carries no spelling tag; it keeps the
11342                    // canonical `AS`, unchanged by this fidelity pass.
11343                    render_alias(with_offset_alias.as_ref(), AliasSpelling::As, ctx, f)?;
11344                }
11345                Ok(())
11346            }
11347            TableFactor::NestedJoin { table, alias, .. } => {
11348                f.write_str("(")?;
11349                table.render(ctx, f)?;
11350                f.write_str(")")?;
11351                render_table_alias(alias.as_deref(), ctx, f)
11352            }
11353            TableFactor::SpecialFunction {
11354                keyword,
11355                precision,
11356                alias,
11357                ..
11358            } => {
11359                f.write_str(special_function_keyword(*keyword))?;
11360                if let Some(precision) = precision {
11361                    write!(f, "({precision})")?;
11362                }
11363                render_table_alias(alias.as_deref(), ctx, f)
11364            }
11365            // A statement-spelled core in factor position is the parenthesized
11366            // statement form (`FROM (PIVOT t ON …)`), so the parentheses rederive
11367            // from the spelling tag; the suffix spelling needs none of its own.
11368            TableFactor::Pivot { pivot, alias, .. } => {
11369                if matches!(pivot.spelling, PivotSpelling::Statement) {
11370                    f.write_str("(")?;
11371                    pivot.render(ctx, f)?;
11372                    f.write_str(")")?;
11373                } else {
11374                    pivot.render(ctx, f)?;
11375                }
11376                render_table_alias(alias.as_deref(), ctx, f)
11377            }
11378            TableFactor::Unpivot { unpivot, alias, .. } => {
11379                if matches!(unpivot.spelling, UnpivotSpelling::Statement) {
11380                    f.write_str("(")?;
11381                    unpivot.render(ctx, f)?;
11382                    f.write_str(")")?;
11383                } else {
11384                    unpivot.render(ctx, f)?;
11385                }
11386                render_table_alias(alias.as_deref(), ctx, f)
11387            }
11388            TableFactor::MatchRecognize {
11389                match_recognize,
11390                alias,
11391                ..
11392            } => {
11393                match_recognize.render(ctx, f)?;
11394                render_table_alias(alias.as_deref(), ctx, f)
11395            }
11396            // DuckDB's `SHOW_REF` table source is always written parenthesized in
11397            // `FROM` (`FROM (DESCRIBE …)`, `FROM (SHOW databases)`); the parentheses are
11398            // load-bearing (a bare leading keyword is a top-level statement), so they
11399            // render unconditionally, like the statement-spelled `TableFactor::Pivot`.
11400            TableFactor::ShowRef { show, alias, .. } => {
11401                f.write_str("(")?;
11402                show.render(ctx, f)?;
11403                f.write_str(")")?;
11404                render_table_alias(alias.as_deref(), ctx, f)
11405            }
11406            TableFactor::JsonTable {
11407                json_table, alias, ..
11408            } => {
11409                json_table.render(ctx, f)?;
11410                render_table_alias(alias.as_deref(), ctx, f)
11411            }
11412            TableFactor::XmlTable {
11413                xml_table, alias, ..
11414            } => {
11415                xml_table.render(ctx, f)?;
11416                render_table_alias(alias.as_deref(), ctx, f)
11417            }
11418            TableFactor::OpenJson {
11419                open_json, alias, ..
11420            } => {
11421                open_json.render(ctx, f)?;
11422                render_table_alias(alias.as_deref(), ctx, f)
11423            }
11424            TableFactor::TableExpr { expr, alias, .. } => {
11425                f.write_str("TABLE(")?;
11426                expr.render(ctx, f)?;
11427                f.write_str(")")?;
11428                render_table_alias(alias.as_deref(), ctx, f)
11429            }
11430            TableFactor::Other { ext, .. } => ext.render(ctx, f),
11431        }
11432    }
11433}
11434
11435impl<X: Extension + Render> Render for ShowRef<X> {
11436    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11437        f.write_str(match self.kind {
11438            ShowRefKind::Describe => "DESCRIBE ",
11439            ShowRefKind::Show => "SHOW ",
11440            ShowRefKind::Summarize => "SUMMARIZE ",
11441        })?;
11442        match &self.target {
11443            ShowRefTarget::Query { query, .. } => query.render(ctx, f),
11444            ShowRefTarget::Name { name, .. } => name.render(ctx, f),
11445        }
11446    }
11447}
11448
11449impl<X: Extension + Render> Render for JsonTable<X> {
11450    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11451        if self.lateral {
11452            f.write_str("LATERAL ")?;
11453        }
11454        f.write_str("JSON_TABLE(")?;
11455        self.context.render(ctx, f)?;
11456        f.write_str(", ")?;
11457        self.path.render(ctx, f)?;
11458        if let Some(name) = &self.path_name {
11459            f.write_str(" AS ")?;
11460            name.render(ctx, f)?;
11461        }
11462        if !self.passing.is_empty() {
11463            f.write_str(" PASSING ")?;
11464            render_comma_separated(&self.passing, ctx, f)?;
11465        }
11466        f.write_str(" COLUMNS (")?;
11467        render_comma_separated(&self.columns, ctx, f)?;
11468        f.write_str(")")?;
11469        render_json_on_behavior(&self.on_error, "ERROR", ctx, f)?;
11470        f.write_str(")")
11471    }
11472}
11473
11474impl<X: Extension + Render> Render for JsonTableColumn<X> {
11475    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11476        match self {
11477            JsonTableColumn::ForOrdinality { name, .. } => {
11478                name.render(ctx, f)?;
11479                f.write_str(" FOR ORDINALITY")
11480            }
11481            JsonTableColumn::Regular {
11482                name,
11483                data_type,
11484                format,
11485                path,
11486                wrapper,
11487                quotes,
11488                on_empty,
11489                on_error,
11490                ..
11491            } => {
11492                name.render(ctx, f)?;
11493                f.write_str(" ")?;
11494                data_type.render(ctx, f)?;
11495                if let Some(format) = format {
11496                    f.write_str(" ")?;
11497                    format.render(ctx, f)?;
11498                }
11499                if let Some(path) = path {
11500                    f.write_str(" PATH ")?;
11501                    path.render(ctx, f)?;
11502                }
11503                render_json_wrapper(*wrapper, f)?;
11504                render_json_quotes(*quotes, f)?;
11505                render_json_on_behavior(on_empty, "EMPTY", ctx, f)?;
11506                render_json_on_behavior(on_error, "ERROR", ctx, f)
11507            }
11508            JsonTableColumn::Exists {
11509                name,
11510                data_type,
11511                path,
11512                on_error,
11513                ..
11514            } => {
11515                name.render(ctx, f)?;
11516                f.write_str(" ")?;
11517                data_type.render(ctx, f)?;
11518                f.write_str(" EXISTS")?;
11519                if let Some(path) = path {
11520                    f.write_str(" PATH ")?;
11521                    path.render(ctx, f)?;
11522                }
11523                render_json_on_behavior(on_error, "ERROR", ctx, f)
11524            }
11525            JsonTableColumn::Nested {
11526                path,
11527                path_name,
11528                columns,
11529                ..
11530            } => {
11531                f.write_str("NESTED PATH ")?;
11532                path.render(ctx, f)?;
11533                if let Some(name) = path_name {
11534                    f.write_str(" AS ")?;
11535                    name.render(ctx, f)?;
11536                }
11537                f.write_str(" COLUMNS (")?;
11538                render_comma_separated(columns, ctx, f)?;
11539                f.write_str(")")
11540            }
11541        }
11542    }
11543}
11544
11545/// Render an expression that occupies a PostgreSQL `c_expr` operand position, wrapping it in
11546/// parentheses when it is a compound `a_expr` (a binary/unary operator, predicate, …) rather
11547/// than a `c_expr` primary. In a `c_expr` slot a bare `a || b` is a syntax error, so a parsed
11548/// `(a || b)` must re-emit its parentheses to round-trip; a primary (a literal, column,
11549/// function call, parenthesized/atom form) needs none. Redundant parentheses on a primary are
11550/// harmless — PostgreSQL folds them away — so the allowlist errs toward the common atoms.
11551fn render_c_expr_operand<X: Extension + Render>(
11552    expr: &Expr<X>,
11553    ctx: &RenderCtx<'_>,
11554    f: &mut fmt::Formatter<'_>,
11555) -> fmt::Result {
11556    let is_primary = matches!(
11557        expr,
11558        Expr::Column { .. }
11559            | Expr::Literal { .. }
11560            | Expr::Function { .. }
11561            | Expr::Parameter { .. }
11562            | Expr::PositionalColumn { .. }
11563            | Expr::SessionVariable { .. }
11564            | Expr::SpecialFunction { .. }
11565    );
11566    if is_primary {
11567        expr.render(ctx, f)
11568    } else {
11569        f.write_str("(")?;
11570        expr.render(ctx, f)?;
11571        f.write_str(")")
11572    }
11573}
11574
11575impl<X: Extension + Render> Render for XmlTable<X> {
11576    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11577        if self.lateral {
11578            f.write_str("LATERAL ")?;
11579        }
11580        f.write_str("XMLTABLE(")?;
11581        if !self.namespaces.is_empty() {
11582            f.write_str("XMLNAMESPACES(")?;
11583            render_comma_separated(&self.namespaces, ctx, f)?;
11584            f.write_str("), ")?;
11585        }
11586        render_c_expr_operand(&self.row_expr, ctx, f)?;
11587        f.write_str(" PASSING")?;
11588        render_xml_passing_mechanism(&self.passing_mechanism_before, f)?;
11589        f.write_str(" ")?;
11590        render_c_expr_operand(&self.document, ctx, f)?;
11591        render_xml_passing_mechanism(&self.passing_mechanism_after, f)?;
11592        f.write_str(" COLUMNS ")?;
11593        render_comma_separated(&self.columns, ctx, f)?;
11594        f.write_str(")")
11595    }
11596}
11597
11598impl<X: Extension + Render> Render for XmlNamespace<X> {
11599    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11600        match &self.name {
11601            Some(name) => {
11602                self.uri.render(ctx, f)?;
11603                f.write_str(" AS ")?;
11604                name.render(ctx, f)
11605            }
11606            None => {
11607                f.write_str("DEFAULT ")?;
11608                self.uri.render(ctx, f)
11609            }
11610        }
11611    }
11612}
11613
11614impl<X: Extension + Render> Render for XmlTableColumn<X> {
11615    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11616        match self {
11617            XmlTableColumn::ForOrdinality { name, .. } => {
11618                name.render(ctx, f)?;
11619                f.write_str(" FOR ORDINALITY")
11620            }
11621            XmlTableColumn::Regular {
11622                name,
11623                data_type,
11624                path,
11625                default,
11626                not_null,
11627                ..
11628            } => {
11629                name.render(ctx, f)?;
11630                f.write_str(" ")?;
11631                data_type.render(ctx, f)?;
11632                // Canonical order: PATH, DEFAULT, then the nullability declaration.
11633                // PostgreSQL admits these order-free and normalizes them into fixed
11634                // fields, so re-emitting one order round-trips to the same node.
11635                if let Some(path) = path {
11636                    f.write_str(" PATH ")?;
11637                    path.render(ctx, f)?;
11638                }
11639                if let Some(default) = default {
11640                    f.write_str(" DEFAULT ")?;
11641                    default.render(ctx, f)?;
11642                }
11643                match not_null {
11644                    Some(true) => f.write_str(" NOT NULL"),
11645                    Some(false) => f.write_str(" NULL"),
11646                    None => Ok(()),
11647                }
11648            }
11649        }
11650    }
11651}
11652
11653impl<X: Extension + Render> Render for OpenJson<X> {
11654    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11655        f.write_str("OPENJSON(")?;
11656        self.json_expr.render(ctx, f)?;
11657        if let Some(path) = &self.path {
11658            f.write_str(", ")?;
11659            path.render(ctx, f)?;
11660        }
11661        f.write_str(")")?;
11662        // An empty `columns` is the absent `WITH` clause (the default schema); a present
11663        // clause is always non-empty.
11664        if !self.columns.is_empty() {
11665            f.write_str(" WITH (")?;
11666            render_comma_separated(&self.columns, ctx, f)?;
11667            f.write_str(")")?;
11668        }
11669        Ok(())
11670    }
11671}
11672
11673impl<X: Extension + Render> Render for OpenJsonColumn<X> {
11674    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11675        self.name.render(ctx, f)?;
11676        f.write_str(" ")?;
11677        self.data_type.render(ctx, f)?;
11678        if let Some(path) = &self.path {
11679            f.write_str(" ")?;
11680            path.render(ctx, f)?;
11681        }
11682        if self.as_json {
11683            f.write_str(" AS JSON")?;
11684        }
11685        Ok(())
11686    }
11687}
11688
11689impl<X: Extension + Render> Render for Pivot<X> {
11690    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11691        match self.spelling {
11692            // `[WITH …] PIVOT <source> [ON …] [USING …] [GROUP BY …] [ORDER BY …]
11693            // [LIMIT …]` — the leading-keyword statement; every clause but the
11694            // source may be absent.
11695            PivotSpelling::Statement => {
11696                if let Some(with) = &self.with {
11697                    with.render(ctx, f)?;
11698                    f.write_str(" ")?;
11699                }
11700                f.write_str("PIVOT ")?;
11701                self.source.render(ctx, f)?;
11702                if !self.pivot_on.is_empty() {
11703                    f.write_str(" ON ")?;
11704                    render_comma_separated(&self.pivot_on, ctx, f)?;
11705                }
11706                if !self.aggregates.is_empty() {
11707                    f.write_str(" USING ")?;
11708                    render_comma_separated(&self.aggregates, ctx, f)?;
11709                }
11710                if !self.group_by.is_empty() {
11711                    f.write_str(" GROUP BY ")?;
11712                    render_comma_separated(&self.group_by, ctx, f)?;
11713                }
11714                render_pivot_statement_tail(
11715                    &self.order_by,
11716                    self.order_by_all.as_deref(),
11717                    self.limit.as_deref(),
11718                    ctx,
11719                    f,
11720                )
11721            }
11722            // `<source> PIVOT (<aggregates> FOR <col> IN (<values>) [GROUP BY …])` — the
11723            // table factor; exactly one `FOR` column (the enclosing alias is rendered by
11724            // the `TableFactor::Pivot` arm).
11725            PivotSpelling::TableFactor => {
11726                self.source.render(ctx, f)?;
11727                f.write_str(" PIVOT (")?;
11728                render_comma_separated(&self.aggregates, ctx, f)?;
11729                // One `FOR` keyword heads the whole column list; the extra heads
11730                // are written bare, space-separated (`FOR y IN (…) m IN (…)`).
11731                for (index, column) in self.pivot_on.iter().enumerate() {
11732                    f.write_str(if index == 0 { " FOR " } else { " " })?;
11733                    column.render(ctx, f)?;
11734                }
11735                if !self.group_by.is_empty() {
11736                    f.write_str(" GROUP BY ")?;
11737                    render_comma_separated(&self.group_by, ctx, f)?;
11738                }
11739                if let Some(default) = &self.default_on_null {
11740                    f.write_str(" DEFAULT ON NULL (")?;
11741                    default.render(ctx, f)?;
11742                    f.write_str(")")?;
11743                }
11744                f.write_str(")")
11745            }
11746        }
11747    }
11748}
11749
11750impl<X: Extension + Render> Render for Unpivot<X> {
11751    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11752        match self.spelling {
11753            // `[WITH …] UNPIVOT <source> ON <cols> [INTO NAME <name> VALUE <value>]
11754            // [ORDER BY …] [LIMIT …]`.
11755            UnpivotSpelling::Statement => {
11756                if let Some(with) = &self.with {
11757                    with.render(ctx, f)?;
11758                    f.write_str(" ")?;
11759                }
11760                f.write_str("UNPIVOT ")?;
11761                self.source.render(ctx, f)?;
11762                f.write_str(" ON ")?;
11763                render_comma_separated(&self.columns, ctx, f)?;
11764                if !self.name.is_empty() || !self.value.is_empty() {
11765                    f.write_str(" INTO NAME ")?;
11766                    render_comma_separated(&self.name, ctx, f)?;
11767                    f.write_str(" VALUE ")?;
11768                    render_comma_separated(&self.value, ctx, f)?;
11769                }
11770                render_pivot_statement_tail(
11771                    &self.order_by,
11772                    self.order_by_all.as_deref(),
11773                    self.limit.as_deref(),
11774                    ctx,
11775                    f,
11776                )
11777            }
11778            // `<source> UNPIVOT [INCLUDE|EXCLUDE NULLS] (<value> FOR <name> IN (<cols>))`.
11779            UnpivotSpelling::TableFactor => {
11780                self.source.render(ctx, f)?;
11781                f.write_str(" UNPIVOT ")?;
11782                // A written marker round-trips; the unwritten default (`None`) renders
11783                // bare (`EXCLUDE NULLS` semantics).
11784                match self.null_inclusion {
11785                    Some(NullInclusion::IncludeNulls) => f.write_str("INCLUDE NULLS ")?,
11786                    Some(NullInclusion::ExcludeNulls) => f.write_str("EXCLUDE NULLS ")?,
11787                    None => {}
11788                }
11789                f.write_str("(")?;
11790                render_unpivot_name_list(&self.value, ctx, f)?;
11791                f.write_str(" FOR ")?;
11792                render_unpivot_name_list(&self.name, ctx, f)?;
11793                f.write_str(" IN (")?;
11794                render_comma_separated(&self.columns, ctx, f)?;
11795                f.write_str("))")
11796            }
11797        }
11798    }
11799}
11800
11801/// Render a row-pattern list joined by `separator` (`" "` for concatenation, `" | "`
11802/// for alternation, `", "` for `PERMUTE`).
11803fn render_row_patterns(
11804    patterns: &[MatchRecognizePattern],
11805    separator: &str,
11806    ctx: &RenderCtx<'_>,
11807    f: &mut fmt::Formatter<'_>,
11808) -> fmt::Result {
11809    for (index, pattern) in patterns.iter().enumerate() {
11810        if index > 0 {
11811            f.write_str(separator)?;
11812        }
11813        pattern.render(ctx, f)?;
11814    }
11815    Ok(())
11816}
11817
11818impl<X: Extension + Render> Render for MatchRecognize<X> {
11819    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11820        // `<source> MATCH_RECOGNIZE ( [PARTITION BY …] [ORDER BY …] [MEASURES …]
11821        // [ROWS PER MATCH] [AFTER MATCH SKIP …] PATTERN (…) [SUBSET …] [DEFINE …] )`.
11822        // Every clause but `PATTERN` is optional; `wrote` tracks whether a preceding
11823        // clause has been emitted so the single-space separators stay tidy.
11824        self.source.render(ctx, f)?;
11825        f.write_str(" MATCH_RECOGNIZE (")?;
11826        let mut wrote = false;
11827        if !self.partition_by.is_empty() {
11828            f.write_str("PARTITION BY ")?;
11829            render_comma_separated(&self.partition_by, ctx, f)?;
11830            wrote = true;
11831        }
11832        if !self.order_by.is_empty() {
11833            if wrote {
11834                f.write_str(" ")?;
11835            }
11836            f.write_str("ORDER BY ")?;
11837            render_comma_separated(&self.order_by, ctx, f)?;
11838            wrote = true;
11839        }
11840        if !self.measures.is_empty() {
11841            if wrote {
11842                f.write_str(" ")?;
11843            }
11844            f.write_str("MEASURES ")?;
11845            render_comma_separated(&self.measures, ctx, f)?;
11846            wrote = true;
11847        }
11848        if let Some(rows_per_match) = &self.rows_per_match {
11849            if wrote {
11850                f.write_str(" ")?;
11851            }
11852            rows_per_match.render(ctx, f)?;
11853            wrote = true;
11854        }
11855        if let Some(after_match_skip) = &self.after_match_skip {
11856            if wrote {
11857                f.write_str(" ")?;
11858            }
11859            after_match_skip.render(ctx, f)?;
11860            wrote = true;
11861        }
11862        if wrote {
11863            f.write_str(" ")?;
11864        }
11865        f.write_str("PATTERN (")?;
11866        self.pattern.render(ctx, f)?;
11867        f.write_str(")")?;
11868        if !self.subsets.is_empty() {
11869            f.write_str(" SUBSET ")?;
11870            render_comma_separated(&self.subsets, ctx, f)?;
11871        }
11872        if !self.define.is_empty() {
11873            f.write_str(" DEFINE ")?;
11874            render_comma_separated(&self.define, ctx, f)?;
11875        }
11876        f.write_str(")")
11877    }
11878}
11879
11880impl<X: Extension + Render> Render for Measure<X> {
11881    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11882        self.expr.render(ctx, f)?;
11883        f.write_str(" AS ")?;
11884        self.alias.render(ctx, f)
11885    }
11886}
11887
11888impl Render for RowsPerMatch {
11889    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11890        match self {
11891            RowsPerMatch::OneRow => f.write_str("ONE ROW PER MATCH"),
11892            RowsPerMatch::AllRows(mode) => {
11893                f.write_str("ALL ROWS PER MATCH")?;
11894                if let Some(mode) = mode {
11895                    f.write_str(" ")?;
11896                    mode.render(ctx, f)?;
11897                }
11898                Ok(())
11899            }
11900        }
11901    }
11902}
11903
11904impl Render for EmptyMatchesMode {
11905    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11906        f.write_str(match self {
11907            EmptyMatchesMode::Show => "SHOW EMPTY MATCHES",
11908            EmptyMatchesMode::Omit => "OMIT EMPTY MATCHES",
11909            EmptyMatchesMode::WithUnmatched => "WITH UNMATCHED ROWS",
11910        })
11911    }
11912}
11913
11914impl Render for AfterMatchSkip {
11915    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11916        f.write_str("AFTER MATCH SKIP ")?;
11917        match self {
11918            AfterMatchSkip::PastLastRow { .. } => f.write_str("PAST LAST ROW"),
11919            AfterMatchSkip::ToNextRow { .. } => f.write_str("TO NEXT ROW"),
11920            AfterMatchSkip::ToFirst { symbol, .. } => {
11921                f.write_str("TO FIRST ")?;
11922                symbol.render(ctx, f)
11923            }
11924            AfterMatchSkip::ToLast { symbol, .. } => {
11925                f.write_str("TO LAST ")?;
11926                symbol.render(ctx, f)
11927            }
11928        }
11929    }
11930}
11931
11932impl Render for SubsetDefinition {
11933    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11934        self.name.render(ctx, f)?;
11935        f.write_str(" = (")?;
11936        render_comma_separated(&self.members, ctx, f)?;
11937        f.write_str(")")
11938    }
11939}
11940
11941impl<X: Extension + Render> Render for SymbolDefinition<X> {
11942    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11943        self.symbol.render(ctx, f)?;
11944        f.write_str(" AS ")?;
11945        self.definition.render(ctx, f)
11946    }
11947}
11948
11949impl Render for MatchRecognizePattern {
11950    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11951        match self {
11952            MatchRecognizePattern::Symbol { symbol, .. } => symbol.render(ctx, f),
11953            MatchRecognizePattern::Start { .. } => f.write_str("^"),
11954            MatchRecognizePattern::End { .. } => f.write_str("$"),
11955            MatchRecognizePattern::Concat { patterns, .. } => {
11956                render_row_patterns(patterns, " ", ctx, f)
11957            }
11958            MatchRecognizePattern::Alternation { patterns, .. } => {
11959                render_row_patterns(patterns, " | ", ctx, f)
11960            }
11961            MatchRecognizePattern::Group { pattern, .. } => {
11962                f.write_str("(")?;
11963                pattern.render(ctx, f)?;
11964                f.write_str(")")
11965            }
11966            MatchRecognizePattern::Exclude { pattern, .. } => {
11967                f.write_str("{- ")?;
11968                pattern.render(ctx, f)?;
11969                f.write_str(" -}")
11970            }
11971            MatchRecognizePattern::Permute { patterns, .. } => {
11972                f.write_str("PERMUTE(")?;
11973                render_row_patterns(patterns, ", ", ctx, f)?;
11974                f.write_str(")")
11975            }
11976            MatchRecognizePattern::Repetition {
11977                pattern,
11978                quantifier,
11979                ..
11980            } => {
11981                pattern.render(ctx, f)?;
11982                quantifier.render(ctx, f)
11983            }
11984        }
11985    }
11986}
11987
11988impl Render for RepetitionQuantifier {
11989    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11990        match self {
11991            RepetitionQuantifier::ZeroOrMore => f.write_str("*"),
11992            RepetitionQuantifier::OneOrMore => f.write_str("+"),
11993            RepetitionQuantifier::AtMostOne => f.write_str("?"),
11994            RepetitionQuantifier::Exactly(n) => write!(f, "{{{n}}}"),
11995            RepetitionQuantifier::AtLeast(n) => write!(f, "{{{n},}}"),
11996            RepetitionQuantifier::AtMost(m) => write!(f, "{{,{m}}}"),
11997            RepetitionQuantifier::Range(n, m) => write!(f, "{{{n},{m}}}"),
11998        }
11999    }
12000}
12001
12002impl<X: Extension + Render> Render for PivotExpr<X> {
12003    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12004        self.expr.render(ctx, f)?;
12005        render_alias(self.alias.as_ref(), self.alias_spelling, ctx, f)
12006    }
12007}
12008
12009impl<X: Extension + Render> Render for PivotValueSource<X> {
12010    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12011        match self {
12012            PivotValueSource::Any { order_by, .. } => {
12013                f.write_str("ANY")?;
12014                if !order_by.is_empty() {
12015                    f.write_str(" ORDER BY ")?;
12016                    render_comma_separated(order_by, ctx, f)?;
12017                }
12018                Ok(())
12019            }
12020            PivotValueSource::Subquery { query, .. } => query.render(ctx, f),
12021        }
12022    }
12023}
12024
12025impl<X: Extension + Render> Render for PivotColumn<X> {
12026    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12027        if let Some(enum_source) = &self.enum_source {
12028            self.expr.render(ctx, f)?;
12029            f.write_str(" IN ")?;
12030            return enum_source.render(ctx, f);
12031        }
12032        // The standard `IN (ANY [ORDER BY …])` / `IN (<subquery>)` sources — the left
12033        // operand rederives its grouping parens exactly like the value-list branch.
12034        if let Some(value_source) = &self.value_source {
12035            render_predicate_operand(
12036                &self.expr,
12037                ctx.target().binding_powers.range_predicate(),
12038                Side::Left,
12039                ctx,
12040                f,
12041            )?;
12042            f.write_str(" IN (")?;
12043            value_source.render(ctx, f)?;
12044            return f.write_str(")");
12045        }
12046        if self.values.is_empty() {
12047            return self.expr.render(ctx, f);
12048        }
12049        // The written `IN` binds at the range-predicate rank exactly like the `IN` predicate
12050        // the statement parse literally reads it as before unfolding, so the column is rendered
12051        // as that predicate's left operand — rederiving the grouping parens a comparison-or-looser
12052        // expression needs (ADR-0008; `(a = b) IN (false, true)` must not re-render as the invalid
12053        // `a = b IN (…)` chain).
12054        render_predicate_operand(
12055            &self.expr,
12056            ctx.target().binding_powers.range_predicate(),
12057            Side::Left,
12058            ctx,
12059            f,
12060        )?;
12061        f.write_str(" IN (")?;
12062        render_comma_separated(&self.values, ctx, f)?;
12063        f.write_str(")")
12064    }
12065}
12066
12067impl<X: Extension + Render> Render for UnpivotColumn<X> {
12068    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12069        // A single column renders bare (`a`); a group parenthesizes (`(a, b)`).
12070        if self.columns.len() == 1 {
12071            self.columns[0].render(ctx, f)?;
12072        } else {
12073            f.write_str("(")?;
12074            render_comma_separated(&self.columns, ctx, f)?;
12075            f.write_str(")")?;
12076        }
12077        render_alias(self.alias.as_ref(), self.alias_spelling, ctx, f)
12078    }
12079}
12080
12081/// Render an `UNPIVOT` value/name list: a single name bare (`v`), several parenthesized
12082/// (`(v1, v2)`), matching DuckDB's multi-column unpivot surface.
12083fn render_unpivot_name_list(
12084    names: &[Ident],
12085    ctx: &RenderCtx<'_>,
12086    f: &mut fmt::Formatter<'_>,
12087) -> fmt::Result {
12088    if names.len() == 1 {
12089        names[0].render(ctx, f)
12090    } else {
12091        f.write_str("(")?;
12092        render_comma_separated(names, ctx, f)?;
12093        f.write_str(")")
12094    }
12095}
12096
12097/// Render the pivot statements' trailing `ORDER BY` / `LIMIT` modifiers (the [`Query`]
12098/// tail pattern, minus the clauses the pivot statements have no grammar for). The
12099/// parser never populates `order_by_all` alongside `order_by` (the engine rejects
12100/// mixing), so the two arms cannot both fire.
12101fn render_pivot_statement_tail<X: Extension + Render>(
12102    order_by: &[OrderByExpr<X>],
12103    order_by_all: Option<&OrderByAll>,
12104    limit: Option<&Limit<X>>,
12105    ctx: &RenderCtx<'_>,
12106    f: &mut fmt::Formatter<'_>,
12107) -> fmt::Result {
12108    if !order_by.is_empty() {
12109        f.write_str(" ORDER BY ")?;
12110        render_comma_separated(order_by, ctx, f)?;
12111    }
12112    if let Some(all) = order_by_all {
12113        f.write_str(" ORDER BY ")?;
12114        all.render(ctx, f)?;
12115    }
12116    if let Some(limit) = limit {
12117        f.write_str(" ")?;
12118        limit.render(ctx, f)?;
12119    }
12120    Ok(())
12121}
12122
12123impl<X: Extension + Render> Render for RowsFromItem<X> {
12124    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12125        self.function.render(ctx, f)?;
12126        if !self.column_defs.is_empty() {
12127            f.write_str(" AS (")?;
12128            render_comma_separated(&self.column_defs, ctx, f)?;
12129            f.write_str(")")?;
12130        }
12131        Ok(())
12132    }
12133}
12134
12135impl<X: Extension + Render> Render for TableFunctionColumn<X> {
12136    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12137        self.name.render(ctx, f)?;
12138        f.write_str(" ")?;
12139        self.data_type.render(ctx, f)
12140    }
12141}
12142
12143impl Render for TableAlias {
12144    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12145        self.name.render(ctx, f)?;
12146        if !self.columns.is_empty() {
12147            f.write_str("(")?;
12148            render_ident_list(&self.columns, ctx, f)?;
12149            f.write_str(")")?;
12150        }
12151        Ok(())
12152    }
12153}
12154
12155impl<X: Extension + Render> Render for TableSample<X> {
12156    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12157        f.write_str(" TABLESAMPLE ")?;
12158        self.method.render(ctx, f)?;
12159        f.write_str(" (")?;
12160        render_comma_separated(&self.args, ctx, f)?;
12161        f.write_str(")")?;
12162        if let Some(repeatable) = &self.repeatable {
12163            f.write_str(" REPEATABLE (")?;
12164            repeatable.render(ctx, f)?;
12165            f.write_str(")")?;
12166        }
12167        Ok(())
12168    }
12169}
12170
12171impl Render for SampleClause {
12172    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12173        // Canonical (method-first) spelling of DuckDB's two equivalent entry surfaces
12174        // (ADR-0011): a named method wraps the count `method(size unit)`, then the seed
12175        // rides a trailing `REPEATABLE (seed)`; a bare count renders `size unit`. The
12176        // caller has already written the `USING SAMPLE ` lead.
12177        if let Some(method) = &self.method {
12178            method.render(ctx, f)?;
12179            f.write_str("(")?;
12180            self.size.render(ctx, f)?;
12181            self.unit.render(ctx, f)?;
12182            f.write_str(")")?;
12183            if let Some(seed) = &self.seed {
12184                f.write_str(" REPEATABLE (")?;
12185                seed.render(ctx, f)?;
12186                f.write_str(")")?;
12187            }
12188        } else {
12189            self.size.render(ctx, f)?;
12190            self.unit.render(ctx, f)?;
12191        }
12192        Ok(())
12193    }
12194}
12195
12196impl Render for SampleUnit {
12197    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12198        f.write_str(match self {
12199            SampleUnit::Count => "",
12200            SampleUnit::Rows => " ROWS",
12201            SampleUnit::Percent => " PERCENT",
12202            SampleUnit::PercentSign => "%",
12203        })
12204    }
12205}
12206
12207/// Whether this render honours the source alias-introducer tag ([`AliasSpelling`]),
12208/// versus normalizing every alias to the canonical `AS`.
12209///
12210/// Only a source-fidelity [`RenderSpelling::PreserveSource`] render honours it. A
12211/// `TargetDialect` re-spell canonicalizes to `AS` (the prefix-colon form has no
12212/// target-neutral spelling); the [`Redacted`](RenderMode::Redacted) fingerprint also
12213/// canonicalizes, so two statements differing only in `AS`-vs-bare share one
12214/// fingerprint — an alias introducer is cosmetic, like the keyword casing the mask
12215/// already erases.
12216fn honours_alias_spelling(ctx: &RenderCtx<'_>) -> bool {
12217    honours_source_spelling(ctx)
12218}
12219
12220/// Whether a source-spelling surface tag is replayed (a fidelity render) versus
12221/// normalized to its canonical spelling.
12222///
12223/// Only a source-fidelity [`RenderSpelling::PreserveSource`] render outside the
12224/// [`Redacted`](RenderMode::Redacted) fingerprint honours such a tag. A `TargetDialect`
12225/// re-spell emits the canonical form, and the redacted fingerprint canonicalizes too —
12226/// so two statements differing only in a cosmetic spelling (`<>` vs `!=`, `LEFT JOIN`
12227/// vs `LEFT OUTER JOIN`, `SET x = 1` vs `SET x TO 1`) share one fingerprint. The
12228/// operator/keyword spelling is cosmetic, like the keyword casing the mask already
12229/// erases. Shared by every keyword/operator spelling tag in this doctrine.
12230fn honours_source_spelling(ctx: &RenderCtx<'_>) -> bool {
12231    ctx.spelling() == RenderSpelling::PreserveSource && ctx.mode() != RenderMode::Redacted
12232}
12233
12234/// The lead written before an alias name: ` AS ` for an `AS`-introduced or
12235/// synthesized alias, a bare ` ` when the source omitted `AS` and the render honours
12236/// that ([`honours_alias_spelling`]). Mirrors the operator spelling tags: a fidelity
12237/// distinction, not a semantic one.
12238fn alias_lead(spelling: AliasSpelling, ctx: &RenderCtx<'_>) -> &'static str {
12239    if !honours_alias_spelling(ctx) {
12240        return " AS ";
12241    }
12242    match spelling {
12243        AliasSpelling::Bare => " ",
12244        AliasSpelling::As | AliasSpelling::PrefixColon => " AS ",
12245    }
12246}
12247
12248fn render_table_alias(
12249    alias: Option<&TableAlias>,
12250    ctx: &RenderCtx<'_>,
12251    f: &mut fmt::Formatter<'_>,
12252) -> fmt::Result {
12253    if let Some(alias) = alias {
12254        f.write_str(alias_lead(alias.spelling, ctx))?;
12255        alias.render(ctx, f)?;
12256    }
12257    Ok(())
12258}
12259
12260/// Render a table function's alias plus its column definition list. An empty
12261/// `column_defs` is a plain alias; otherwise the typed list renders as
12262/// `AS [name](col type, ...)` — PostgreSQL's record-returning form, where the
12263/// optional correlation name precedes the parenthesized definitions.
12264fn render_function_alias<X: Extension + Render>(
12265    alias: Option<&TableAlias>,
12266    column_defs: &[TableFunctionColumn<X>],
12267    ctx: &RenderCtx<'_>,
12268    f: &mut fmt::Formatter<'_>,
12269) -> fmt::Result {
12270    if column_defs.is_empty() {
12271        return render_table_alias(alias, ctx, f);
12272    }
12273    // The record-returning form `func(...) AS [name](col type, ...)`: the `AS` leads
12274    // the definition list even when the correlation name is elided, so a bare source
12275    // alias only drops the keyword, never the required column list.
12276    f.write_str(alias.map_or(" AS ", |a| alias_lead(a.spelling, ctx)))?;
12277    if let Some(alias) = alias {
12278        alias.name.render(ctx, f)?;
12279    }
12280    f.write_str("(")?;
12281    render_comma_separated(column_defs, ctx, f)?;
12282    f.write_str(")")
12283}
12284
12285fn render_alias(
12286    alias: Option<&Ident>,
12287    spelling: AliasSpelling,
12288    ctx: &RenderCtx<'_>,
12289    f: &mut fmt::Formatter<'_>,
12290) -> fmt::Result {
12291    if let Some(alias) = alias {
12292        f.write_str(alias_lead(spelling, ctx))?;
12293        alias.render(ctx, f)?;
12294    }
12295    Ok(())
12296}
12297
12298/// Render a PostgreSQL `relation_expr` reference: the relation `name` wrapped by
12299/// its inheritance marker. Shared by `TableFactor::Table` and `DmlTarget` so the
12300/// four spellings round-trip identically wherever a relation can appear.
12301fn render_relation_inheritance(
12302    inheritance: &RelationInheritance,
12303    name: &ObjectName,
12304    ctx: &RenderCtx<'_>,
12305    f: &mut fmt::Formatter<'_>,
12306) -> fmt::Result {
12307    match inheritance {
12308        RelationInheritance::Plain => name.render(ctx, f),
12309        RelationInheritance::Descendants => {
12310            name.render(ctx, f)?;
12311            f.write_str(" *")
12312        }
12313        RelationInheritance::Only(OnlySyntax::Bare) => {
12314            f.write_str("ONLY ")?;
12315            name.render(ctx, f)
12316        }
12317        RelationInheritance::Only(OnlySyntax::Parenthesized) => {
12318            f.write_str("ONLY (")?;
12319            name.render(ctx, f)?;
12320            f.write_str(")")
12321        }
12322    }
12323}
12324
12325fn render_ident_list(
12326    items: &[Ident],
12327    ctx: &RenderCtx<'_>,
12328    f: &mut fmt::Formatter<'_>,
12329) -> fmt::Result {
12330    render_comma_separated(items, ctx, f)
12331}
12332
12333/// Render a slice of renderable nodes joined by `, `.
12334fn render_comma_separated<T: Render>(
12335    items: &[T],
12336    ctx: &RenderCtx<'_>,
12337    f: &mut fmt::Formatter<'_>,
12338) -> fmt::Result {
12339    for (i, item) in items.iter().enumerate() {
12340        if i > 0 {
12341            f.write_str(", ")?;
12342        }
12343        item.render(ctx, f)?;
12344    }
12345    Ok(())
12346}
12347
12348/// Render a comma-separated list attached to a preceding keyword by a single
12349/// leading space (` a, b, c`), or nothing when empty — the shared shape of a
12350/// `SELECT` projection, a `WITH` CTE list, and a transaction-mode list.
12351fn render_leading_space_comma_separated<T: Render>(
12352    items: &[T],
12353    ctx: &RenderCtx<'_>,
12354    f: &mut fmt::Formatter<'_>,
12355) -> fmt::Result {
12356    if items.is_empty() {
12357        return Ok(());
12358    }
12359    f.write_str(" ")?;
12360    render_comma_separated(items, ctx, f)
12361}
12362
12363/// Render the parenthesized row tuples of a `VALUES` clause — `(a, b), (c, d)` —
12364/// shared by `InsertValues` and `Values`. The leading `VALUES ` keyword is the
12365/// caller's; this renders only the comma-separated rows, each a parenthesized
12366/// comma-separated item list.
12367fn render_values_rows<T: Render>(
12368    rows: &[ThinVec<T>],
12369    explicit_row: bool,
12370    ctx: &RenderCtx<'_>,
12371    f: &mut fmt::Formatter<'_>,
12372) -> fmt::Result {
12373    // MySQL spells the query-position constructor `ROW( ... )`; the bare `( ... )` is the
12374    // PostgreSQL/DuckDB/SQLite/ANSI spelling. The flag round-trips the `ROW` keyword.
12375    let open = if explicit_row { "ROW(" } else { "(" };
12376    for (i, row) in rows.iter().enumerate() {
12377        if i > 0 {
12378            f.write_str(", ")?;
12379        }
12380        f.write_str(open)?;
12381        render_comma_separated(row, ctx, f)?;
12382        f.write_str(")")?;
12383    }
12384    Ok(())
12385}
12386
12387impl<X: Extension + Render> Render for Join<X> {
12388    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12389        // Order is `<keyword> <relation> <constraint>`, so the relation sits
12390        // between the operator's keyword phrase and its trailing ON/USING clause.
12391        self.operator.render(ctx, f)?;
12392        f.write_str(" ")?;
12393        self.relation.render(ctx, f)?;
12394        if let Some(constraint) = self.operator.constraint() {
12395            constraint.render(ctx, f)?;
12396        }
12397        Ok(())
12398    }
12399}
12400
12401impl<X: Extension> JoinOperator<X> {
12402    /// The constraint embedded in a constraint-carrying join operator.
12403    fn constraint(&self) -> Option<&JoinConstraint<X>> {
12404        match self {
12405            JoinOperator::Inner { constraint, .. }
12406            | JoinOperator::LeftOuter { constraint, .. }
12407            | JoinOperator::RightOuter { constraint, .. }
12408            | JoinOperator::FullOuter { constraint, .. }
12409            | JoinOperator::AsOf { constraint, .. }
12410            | JoinOperator::Semi { constraint, .. }
12411            | JoinOperator::Anti { constraint, .. } => Some(constraint),
12412            JoinOperator::Cross { .. }
12413            | JoinOperator::Positional { .. }
12414            | JoinOperator::Apply { .. } => None,
12415        }
12416    }
12417}
12418
12419impl<X: Extension + Render> Render for JoinOperator<X> {
12420    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12421        // `NATURAL` is a prefix modifier on the keyword, not a trailing clause.
12422        if matches!(self.constraint(), Some(JoinConstraint::Natural { .. })) {
12423            f.write_str("NATURAL ")?;
12424        }
12425        // The optional `INNER`/`OUTER` keyword is exact-synonym noise: only a
12426        // source-fidelity render replays the written form, a target re-spell and the
12427        // redacted fingerprint collapse to the canonical bare `JOIN` / `LEFT JOIN`.
12428        let fidelity = honours_source_spelling(ctx);
12429        f.write_str(match self {
12430            // The MySQL `straight` surface tag selects the `STRAIGHT_JOIN` keyword;
12431            // both spellings are the one canonical inner-join shape (ADR-0011).
12432            JoinOperator::Inner { straight: true, .. } => "STRAIGHT_JOIN",
12433            JoinOperator::Inner {
12434                straight: false,
12435                inner: true,
12436                ..
12437            } if fidelity => "INNER JOIN",
12438            JoinOperator::Inner {
12439                straight: false, ..
12440            } => "JOIN",
12441            JoinOperator::LeftOuter { outer: true, .. } if fidelity => "LEFT OUTER JOIN",
12442            JoinOperator::LeftOuter { .. } => "LEFT JOIN",
12443            JoinOperator::RightOuter { outer: true, .. } if fidelity => "RIGHT OUTER JOIN",
12444            JoinOperator::RightOuter { .. } => "RIGHT JOIN",
12445            JoinOperator::FullOuter { outer: true, .. } if fidelity => "FULL OUTER JOIN",
12446            JoinOperator::FullOuter { .. } => "FULL JOIN",
12447            // The canonical ASOF spelling records the side, not the `INNER`/`OUTER`
12448            // noise (like the side joins above).
12449            JoinOperator::AsOf { kind, .. } => match kind {
12450                AsOfJoinKind::Inner => "ASOF JOIN",
12451                AsOfJoinKind::Left => "ASOF LEFT JOIN",
12452                AsOfJoinKind::Right => "ASOF RIGHT JOIN",
12453                AsOfJoinKind::Full => "ASOF FULL JOIN",
12454            },
12455            JoinOperator::Cross { .. } => "CROSS JOIN",
12456            JoinOperator::Positional { .. } => "POSITIONAL JOIN",
12457            // DuckDB's side-less `SEMI`/`ANTI` is the whole `join_type`; its `ASOF`
12458            // composition prefixes the keyword and its `NATURAL` one is emitted by the
12459            // prefix above (the two never co-occur, so `asof` is `false` under NATURAL).
12460            // Spark's sided spelling writes the `LEFT`/`RIGHT` keyword instead and never
12461            // composes with `ASOF`/`NATURAL`, so `asof` is always `false` there.
12462            JoinOperator::Semi { asof, side, .. } => match (side, asof) {
12463                (SemiAntiSide::Sideless, false) => "SEMI JOIN",
12464                (SemiAntiSide::Sideless, true) => "ASOF SEMI JOIN",
12465                (SemiAntiSide::Left, _) => "LEFT SEMI JOIN",
12466                (SemiAntiSide::Right, _) => "RIGHT SEMI JOIN",
12467            },
12468            JoinOperator::Anti { asof, side, .. } => match (side, asof) {
12469                (SemiAntiSide::Sideless, false) => "ANTI JOIN",
12470                (SemiAntiSide::Sideless, true) => "ASOF ANTI JOIN",
12471                (SemiAntiSide::Left, _) => "LEFT ANTI JOIN",
12472                (SemiAntiSide::Right, _) => "RIGHT ANTI JOIN",
12473            },
12474            // The `CROSS`/`OUTER` flavour is the whole operator keyword; the right table
12475            // factor renders after it (no constraint, like CROSS/POSITIONAL above).
12476            JoinOperator::Apply { kind, .. } => match kind {
12477                ApplyKind::Cross => "CROSS APPLY",
12478                ApplyKind::Outer => "OUTER APPLY",
12479            },
12480        })
12481    }
12482}
12483
12484impl<X: Extension + Render> Render for JoinConstraint<X> {
12485    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12486        match self {
12487            JoinConstraint::On { expr, .. } => {
12488                f.write_str(" ON ")?;
12489                expr.render(ctx, f)
12490            }
12491            JoinConstraint::Using { columns, alias, .. } => {
12492                f.write_str(" USING (")?;
12493                render_comma_separated(columns, ctx, f)?;
12494                f.write_str(")")?;
12495                if let Some(alias) = alias {
12496                    f.write_str(" AS ")?;
12497                    alias.render(ctx, f)?;
12498                }
12499                Ok(())
12500            }
12501            // `Natural` is emitted as the keyword prefix; `None` has no clause.
12502            JoinConstraint::Natural { .. } | JoinConstraint::None { .. } => Ok(()),
12503        }
12504    }
12505}
12506
12507// ---------------------------------------------------------------------------
12508// ORDER BY / LIMIT
12509// ---------------------------------------------------------------------------
12510
12511impl<X: Extension + Render> Render for ExtractExpr<X> {
12512    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12513        f.write_str("EXTRACT(")?;
12514        self.field.render(ctx, f)?;
12515        f.write_str(" FROM ")?;
12516        self.source.render(ctx, f)?;
12517        f.write_str(")")
12518    }
12519}
12520
12521impl Render for NullTreatment {
12522    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12523        f.write_str(match self {
12524            NullTreatment::IgnoreNulls => "IGNORE NULLS",
12525            NullTreatment::RespectNulls => "RESPECT NULLS",
12526        })
12527    }
12528}
12529
12530// ---------------------------------------------------------------------------
12531// SQL/JSON expression functions (pg-sqljson-expression-functions)
12532// ---------------------------------------------------------------------------
12533
12534impl Render for JsonEncoding {
12535    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12536        f.write_str(match self {
12537            JsonEncoding::Utf8 => "UTF8",
12538            JsonEncoding::Utf16 => "UTF16",
12539            JsonEncoding::Utf32 => "UTF32",
12540        })
12541    }
12542}
12543
12544impl Render for JsonFormat {
12545    /// Renders `FORMAT JSON [ENCODING <enc>]`; callers write the leading space.
12546    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12547        f.write_str("FORMAT JSON")?;
12548        if let Some(encoding) = &self.encoding {
12549            f.write_str(" ENCODING ")?;
12550            encoding.render(ctx, f)?;
12551        }
12552        Ok(())
12553    }
12554}
12555
12556impl<X: Extension + Render> Render for JsonValueExpr<X> {
12557    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12558        self.expr.render(ctx, f)?;
12559        if let Some(format) = &self.format {
12560            f.write_str(" ")?;
12561            format.render(ctx, f)?;
12562        }
12563        Ok(())
12564    }
12565}
12566
12567impl<X: Extension + Render> Render for JsonReturning<X> {
12568    /// Renders `RETURNING <type> [FORMAT JSON …]`; callers write the leading space.
12569    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12570        f.write_str("RETURNING ")?;
12571        self.data_type.render(ctx, f)?;
12572        if let Some(format) = &self.format {
12573            f.write_str(" ")?;
12574            format.render(ctx, f)?;
12575        }
12576        Ok(())
12577    }
12578}
12579
12580impl<X: Extension + Render> Render for JsonPassingArg<X> {
12581    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12582        self.value.render(ctx, f)?;
12583        f.write_str(" AS ")?;
12584        self.name.render(ctx, f)
12585    }
12586}
12587
12588impl<X: Extension + Render> Render for JsonBehavior<X> {
12589    /// Renders the behaviour keyword(s) only; callers append ` ON EMPTY` / ` ON ERROR`.
12590    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12591        match self.kind {
12592            JsonBehaviorKind::Error => f.write_str("ERROR"),
12593            JsonBehaviorKind::Null => f.write_str("NULL"),
12594            JsonBehaviorKind::True => f.write_str("TRUE"),
12595            JsonBehaviorKind::False => f.write_str("FALSE"),
12596            JsonBehaviorKind::Unknown => f.write_str("UNKNOWN"),
12597            JsonBehaviorKind::Empty => f.write_str("EMPTY"),
12598            JsonBehaviorKind::EmptyArray => f.write_str("EMPTY ARRAY"),
12599            JsonBehaviorKind::EmptyObject => f.write_str("EMPTY OBJECT"),
12600            JsonBehaviorKind::Default => {
12601                f.write_str("DEFAULT ")?;
12602                // `default_expr` is always `Some` when the kind is `Default`; the render
12603                // is a no-op otherwise rather than panicking on a malformed node.
12604                if let Some(expr) = &self.default_expr {
12605                    expr.render(ctx, f)?;
12606                }
12607                Ok(())
12608            }
12609        }
12610    }
12611}
12612
12613/// Render an `ON EMPTY` / `ON ERROR` behaviour, when present: ` <behaviour> ON <slot>`.
12614fn render_json_on_behavior<X: Extension + Render>(
12615    behavior: &Option<JsonBehavior<X>>,
12616    slot: &str,
12617    ctx: &RenderCtx<'_>,
12618    f: &mut fmt::Formatter<'_>,
12619) -> fmt::Result {
12620    if let Some(behavior) = behavior {
12621        f.write_str(" ")?;
12622        behavior.render(ctx, f)?;
12623        f.write_str(" ON ")?;
12624        f.write_str(slot)?;
12625    }
12626    Ok(())
12627}
12628
12629/// Render a SQL/JSON `WRAPPER` clause when specified; callers write no leading space.
12630fn render_json_wrapper(wrapper: JsonWrapperBehavior, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12631    match wrapper {
12632        JsonWrapperBehavior::Unspecified => Ok(()),
12633        JsonWrapperBehavior::Without => f.write_str(" WITHOUT WRAPPER"),
12634        JsonWrapperBehavior::Unconditional => f.write_str(" WITH WRAPPER"),
12635        JsonWrapperBehavior::Conditional => f.write_str(" WITH CONDITIONAL WRAPPER"),
12636    }
12637}
12638
12639/// Render a SQL/JSON `QUOTES` clause when specified; callers write no leading space.
12640fn render_json_quotes(quotes: JsonQuotesBehavior, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12641    match quotes {
12642        JsonQuotesBehavior::Unspecified => Ok(()),
12643        JsonQuotesBehavior::Keep => f.write_str(" KEEP QUOTES"),
12644        JsonQuotesBehavior::Omit => f.write_str(" OMIT QUOTES"),
12645    }
12646}
12647
12648/// Render a null-handling clause when present: ` ABSENT ON NULL` / ` NULL ON NULL`.
12649fn render_json_null_clause(
12650    clause: &Option<JsonNullClause>,
12651    f: &mut fmt::Formatter<'_>,
12652) -> fmt::Result {
12653    match clause {
12654        Some(JsonNullClause::AbsentOnNull) => f.write_str(" ABSENT ON NULL"),
12655        Some(JsonNullClause::NullOnNull) => f.write_str(" NULL ON NULL"),
12656        None => Ok(()),
12657    }
12658}
12659
12660/// Render a key-uniqueness clause when present: ` WITH UNIQUE KEYS` / ` WITHOUT UNIQUE KEYS`.
12661fn render_json_unique(unique_keys: Option<bool>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12662    match unique_keys {
12663        Some(true) => f.write_str(" WITH UNIQUE KEYS"),
12664        Some(false) => f.write_str(" WITHOUT UNIQUE KEYS"),
12665        None => Ok(()),
12666    }
12667}
12668
12669impl<X: Extension + Render> Render for JsonFuncExpr<X> {
12670    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12671        f.write_str(match self.kind {
12672            JsonFuncKind::Value => "JSON_VALUE(",
12673            JsonFuncKind::Query => "JSON_QUERY(",
12674            JsonFuncKind::Exists => "JSON_EXISTS(",
12675        })?;
12676        self.context.render(ctx, f)?;
12677        f.write_str(", ")?;
12678        self.path.render(ctx, f)?;
12679        if !self.passing.is_empty() {
12680            f.write_str(" PASSING ")?;
12681            render_comma_separated(&self.passing, ctx, f)?;
12682        }
12683        if let Some(returning) = &self.returning {
12684            f.write_str(" ")?;
12685            returning.render(ctx, f)?;
12686        }
12687        render_json_wrapper(self.wrapper, f)?;
12688        render_json_quotes(self.quotes, f)?;
12689        render_json_on_behavior(&self.on_empty, "EMPTY", ctx, f)?;
12690        render_json_on_behavior(&self.on_error, "ERROR", ctx, f)?;
12691        f.write_str(")")
12692    }
12693}
12694
12695impl<X: Extension + Render> Render for JsonKeyValue<X> {
12696    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12697        self.key.render(ctx, f)?;
12698        f.write_str(match self.spelling {
12699            JsonKeyValueSpelling::Colon => ": ",
12700            JsonKeyValueSpelling::Value => " VALUE ",
12701        })?;
12702        self.value.render(ctx, f)
12703    }
12704}
12705
12706impl<X: Extension + Render> Render for JsonObjectExpr<X> {
12707    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12708        f.write_str("JSON_OBJECT(")?;
12709        render_comma_separated(&self.entries, ctx, f)?;
12710        render_json_null_clause(&self.null_clause, f)?;
12711        render_json_unique(self.unique_keys, f)?;
12712        if let Some(returning) = &self.returning {
12713            if !self.entries.is_empty() || self.null_clause.is_some() || self.unique_keys.is_some()
12714            {
12715                f.write_str(" ")?;
12716            }
12717            returning.render(ctx, f)?;
12718        }
12719        f.write_str(")")
12720    }
12721}
12722
12723impl<X: Extension + Render> Render for JsonArrayExpr<X> {
12724    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12725        f.write_str("JSON_ARRAY(")?;
12726        let empty_body = match &self.body {
12727            JsonArrayBody::Values {
12728                items, null_clause, ..
12729            } => {
12730                render_comma_separated(items, ctx, f)?;
12731                render_json_null_clause(null_clause, f)?;
12732                items.is_empty() && null_clause.is_none()
12733            }
12734            JsonArrayBody::Query { query, format, .. } => {
12735                query.render(ctx, f)?;
12736                if let Some(format) = format {
12737                    f.write_str(" ")?;
12738                    format.render(ctx, f)?;
12739                }
12740                false
12741            }
12742        };
12743        if let Some(returning) = &self.returning {
12744            if !empty_body {
12745                f.write_str(" ")?;
12746            }
12747            returning.render(ctx, f)?;
12748        }
12749        f.write_str(")")
12750    }
12751}
12752
12753impl<X: Extension + Render> Render for JsonAggregateExpr<X> {
12754    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12755        match &self.body {
12756            JsonAggregateBody::Object {
12757                entry, unique_keys, ..
12758            } => {
12759                f.write_str("JSON_OBJECTAGG(")?;
12760                entry.render(ctx, f)?;
12761                render_json_null_clause(&self.null_clause, f)?;
12762                render_json_unique(*unique_keys, f)?;
12763            }
12764            JsonAggregateBody::Array {
12765                value, order_by, ..
12766            } => {
12767                f.write_str("JSON_ARRAYAGG(")?;
12768                value.render(ctx, f)?;
12769                if !order_by.is_empty() {
12770                    f.write_str(" ORDER BY ")?;
12771                    render_comma_separated(order_by, ctx, f)?;
12772                }
12773                render_json_null_clause(&self.null_clause, f)?;
12774            }
12775        }
12776        if let Some(returning) = &self.returning {
12777            f.write_str(" ")?;
12778            returning.render(ctx, f)?;
12779        }
12780        f.write_str(")")?;
12781        if let Some(filter) = &self.filter {
12782            f.write_str(" FILTER (WHERE ")?;
12783            filter.render(ctx, f)?;
12784            f.write_str(")")?;
12785        }
12786        if let Some(over) = &self.over {
12787            f.write_str(" OVER ")?;
12788            over.render(ctx, f)?;
12789        }
12790        Ok(())
12791    }
12792}
12793
12794impl<X: Extension + Render> Render for JsonConstructorExpr<X> {
12795    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12796        f.write_str(match self.kind {
12797            JsonConstructorKind::Json => "JSON(",
12798            JsonConstructorKind::Scalar => "JSON_SCALAR(",
12799            JsonConstructorKind::Serialize => "JSON_SERIALIZE(",
12800        })?;
12801        self.value.render(ctx, f)?;
12802        render_json_unique(self.unique_keys, f)?;
12803        if let Some(returning) = &self.returning {
12804            f.write_str(" ")?;
12805            returning.render(ctx, f)?;
12806        }
12807        f.write_str(")")
12808    }
12809}
12810
12811impl<X: Extension + Render> Render for IsJsonExpr<X> {
12812    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12813        let full = ctx.mode() == RenderMode::Parenthesized;
12814        open_group(full, f)?;
12815        render_predicate_operand(
12816            &self.expr,
12817            ctx.target().binding_powers.predicate(),
12818            Side::Left,
12819            ctx,
12820            f,
12821        )?;
12822        f.write_str(if self.negated {
12823            " IS NOT JSON"
12824        } else {
12825            " IS JSON"
12826        })?;
12827        f.write_str(match self.item_type {
12828            JsonItemType::Any => "",
12829            JsonItemType::Value => " VALUE",
12830            JsonItemType::Array => " ARRAY",
12831            JsonItemType::Object => " OBJECT",
12832            JsonItemType::Scalar => " SCALAR",
12833        })?;
12834        if self.unique_keys {
12835            f.write_str(" WITH UNIQUE KEYS")?;
12836        }
12837        close_group(full, f)
12838    }
12839}
12840
12841// ---------------------------------------------------------------------------
12842// SQL/XML expression functions (pg-xml-expression-functions)
12843// ---------------------------------------------------------------------------
12844
12845impl Render for XmlDocumentOrContent {
12846    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12847        f.write_str(match self {
12848            XmlDocumentOrContent::Document => "DOCUMENT",
12849            XmlDocumentOrContent::Content => "CONTENT",
12850        })
12851    }
12852}
12853
12854impl<X: Extension + Render> Render for XmlAttribute<X> {
12855    /// Renders `<value> [AS <name>]` — one `xmlattributes` / `xmlforest` element.
12856    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12857        self.value.render(ctx, f)?;
12858        if let Some(name) = &self.name {
12859            f.write_str(" AS ")?;
12860            name.render(ctx, f)?;
12861        }
12862        Ok(())
12863    }
12864}
12865
12866/// Render an optional `xmlexists` passing mechanism: ` BY REF` / ` BY VALUE`.
12867fn render_xml_passing_mechanism(
12868    mechanism: &Option<XmlPassingMechanism>,
12869    f: &mut fmt::Formatter<'_>,
12870) -> fmt::Result {
12871    match mechanism {
12872        Some(XmlPassingMechanism::ByRef) => f.write_str(" BY REF"),
12873        Some(XmlPassingMechanism::ByValue) => f.write_str(" BY VALUE"),
12874        None => Ok(()),
12875    }
12876}
12877
12878impl<X: Extension + Render> Render for XmlFunc<X> {
12879    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12880        match self {
12881            XmlFunc::Element {
12882                name,
12883                attributes,
12884                content,
12885                ..
12886            } => {
12887                f.write_str("xmlelement(NAME ")?;
12888                name.render(ctx, f)?;
12889                if !attributes.is_empty() {
12890                    f.write_str(", xmlattributes(")?;
12891                    render_comma_separated(attributes, ctx, f)?;
12892                    f.write_str(")")?;
12893                }
12894                if !content.is_empty() {
12895                    f.write_str(", ")?;
12896                    render_comma_separated(content, ctx, f)?;
12897                }
12898                f.write_str(")")
12899            }
12900            XmlFunc::Forest { elements, .. } => {
12901                f.write_str("xmlforest(")?;
12902                render_comma_separated(elements, ctx, f)?;
12903                f.write_str(")")
12904            }
12905            XmlFunc::Concat { args, .. } => {
12906                f.write_str("xmlconcat(")?;
12907                render_comma_separated(args, ctx, f)?;
12908                f.write_str(")")
12909            }
12910            XmlFunc::Parse {
12911                option,
12912                arg,
12913                whitespace,
12914                ..
12915            } => {
12916                f.write_str("xmlparse(")?;
12917                option.render(ctx, f)?;
12918                f.write_str(" ")?;
12919                arg.render(ctx, f)?;
12920                match whitespace {
12921                    XmlWhitespaceOption::Unspecified => {}
12922                    XmlWhitespaceOption::Preserve => f.write_str(" PRESERVE WHITESPACE")?,
12923                    XmlWhitespaceOption::Strip => f.write_str(" STRIP WHITESPACE")?,
12924                }
12925                f.write_str(")")
12926            }
12927            XmlFunc::Pi { name, content, .. } => {
12928                f.write_str("xmlpi(NAME ")?;
12929                name.render(ctx, f)?;
12930                if let Some(content) = content {
12931                    f.write_str(", ")?;
12932                    content.render(ctx, f)?;
12933                }
12934                f.write_str(")")
12935            }
12936            XmlFunc::Root {
12937                arg,
12938                version,
12939                standalone,
12940                ..
12941            } => {
12942                f.write_str("xmlroot(")?;
12943                arg.render(ctx, f)?;
12944                f.write_str(", VERSION ")?;
12945                match version {
12946                    Some(expr) => expr.render(ctx, f)?,
12947                    None => f.write_str("NO VALUE")?,
12948                }
12949                match standalone {
12950                    XmlStandalone::Unspecified => {}
12951                    XmlStandalone::Yes => f.write_str(", STANDALONE YES")?,
12952                    XmlStandalone::No => f.write_str(", STANDALONE NO")?,
12953                    XmlStandalone::NoValue => f.write_str(", STANDALONE NO VALUE")?,
12954                }
12955                f.write_str(")")
12956            }
12957            XmlFunc::Serialize {
12958                option,
12959                arg,
12960                data_type,
12961                indent,
12962                ..
12963            } => {
12964                f.write_str("xmlserialize(")?;
12965                option.render(ctx, f)?;
12966                f.write_str(" ")?;
12967                arg.render(ctx, f)?;
12968                f.write_str(" AS ")?;
12969                data_type.render(ctx, f)?;
12970                match indent {
12971                    XmlIndentOption::Unspecified => {}
12972                    XmlIndentOption::Indent => f.write_str(" INDENT")?,
12973                    XmlIndentOption::NoIndent => f.write_str(" NO INDENT")?,
12974                }
12975                f.write_str(")")
12976            }
12977            XmlFunc::Exists {
12978                path,
12979                mechanism_before,
12980                arg,
12981                mechanism_after,
12982                ..
12983            } => {
12984                f.write_str("xmlexists(")?;
12985                path.render(ctx, f)?;
12986                f.write_str(" PASSING")?;
12987                render_xml_passing_mechanism(mechanism_before, f)?;
12988                f.write_str(" ")?;
12989                arg.render(ctx, f)?;
12990                render_xml_passing_mechanism(mechanism_after, f)?;
12991                f.write_str(")")
12992            }
12993        }
12994    }
12995}
12996
12997impl<X: Extension + Render> Render for StringFunc<X> {
12998    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12999        match self {
13000            // The reversed `FOR … FROM …` source order folds onto the same fields, so
13001            // the render is canonically `FROM`-first; a `FOR`-only form keeps its bare
13002            // `FOR` (there is no `FROM` operand to write).
13003            StringFunc::Substring {
13004                expr, start, count, ..
13005            } => {
13006                f.write_str("SUBSTRING(")?;
13007                expr.render(ctx, f)?;
13008                if let Some(start) = start {
13009                    f.write_str(" FROM ")?;
13010                    start.render(ctx, f)?;
13011                }
13012                if let Some(count) = count {
13013                    f.write_str(" FOR ")?;
13014                    count.render(ctx, f)?;
13015                }
13016                f.write_str(")")
13017            }
13018            StringFunc::SubstringSimilar {
13019                expr,
13020                pattern,
13021                escape,
13022                ..
13023            } => {
13024                f.write_str("SUBSTRING(")?;
13025                expr.render(ctx, f)?;
13026                f.write_str(" SIMILAR ")?;
13027                pattern.render(ctx, f)?;
13028                f.write_str(" ESCAPE ")?;
13029                escape.render(ctx, f)?;
13030                f.write_str(")")
13031            }
13032            StringFunc::Position { substr, string, .. } => {
13033                f.write_str("POSITION(")?;
13034                substr.render(ctx, f)?;
13035                f.write_str(" IN ")?;
13036                string.render(ctx, f)?;
13037                f.write_str(")")
13038            }
13039            StringFunc::Overlay {
13040                target,
13041                replacement,
13042                start,
13043                count,
13044                ..
13045            } => {
13046                f.write_str("OVERLAY(")?;
13047                target.render(ctx, f)?;
13048                f.write_str(" PLACING ")?;
13049                replacement.render(ctx, f)?;
13050                f.write_str(" FROM ")?;
13051                start.render(ctx, f)?;
13052                if let Some(count) = count {
13053                    f.write_str(" FOR ")?;
13054                    count.render(ctx, f)?;
13055                }
13056                f.write_str(")")
13057            }
13058            StringFunc::Trim {
13059                side,
13060                trim_chars,
13061                from,
13062                sources,
13063                ..
13064            } => {
13065                f.write_str("TRIM(")?;
13066                if let Some(side) = side {
13067                    side.render(ctx, f)?;
13068                    f.write_str(" ")?;
13069                }
13070                if let Some(trim_chars) = trim_chars {
13071                    trim_chars.render(ctx, f)?;
13072                    f.write_str(" ")?;
13073                }
13074                if *from {
13075                    f.write_str("FROM ")?;
13076                }
13077                render_comma_separated(sources, ctx, f)?;
13078                f.write_str(")")
13079            }
13080            StringFunc::CollationFor { expr, .. } => {
13081                f.write_str("COLLATION FOR (")?;
13082                expr.render(ctx, f)?;
13083                f.write_str(")")
13084            }
13085            StringFunc::ConvertUsing { expr, charset, .. } => {
13086                f.write_str("CONVERT(")?;
13087                expr.render(ctx, f)?;
13088                f.write_str(" USING ")?;
13089                charset.render(ctx, f)?;
13090                f.write_str(")")
13091            }
13092            StringFunc::MatchAgainst {
13093                columns,
13094                against,
13095                modifier,
13096                ..
13097            } => {
13098                f.write_str("MATCH(")?;
13099                render_comma_separated(columns, ctx, f)?;
13100                f.write_str(") AGAINST(")?;
13101                against.render(ctx, f)?;
13102                if let Some(modifier) = modifier {
13103                    f.write_str(" ")?;
13104                    modifier.render(ctx, f)?;
13105                }
13106                f.write_str(")")
13107            }
13108            StringFunc::CeilTo {
13109                expr,
13110                field,
13111                spelling,
13112                ..
13113            } => {
13114                f.write_str(match spelling {
13115                    CeilSpelling::Ceil => "CEIL(",
13116                    CeilSpelling::Ceiling => "CEILING(",
13117                })?;
13118                expr.render(ctx, f)?;
13119                f.write_str(" TO ")?;
13120                field.render(ctx, f)?;
13121                f.write_str(")")
13122            }
13123            StringFunc::FloorTo { expr, field, .. } => {
13124                f.write_str("FLOOR(")?;
13125                expr.render(ctx, f)?;
13126                f.write_str(" TO ")?;
13127                field.render(ctx, f)?;
13128                f.write_str(")")
13129            }
13130        }
13131    }
13132}
13133
13134impl Render for MatchSearchModifier {
13135    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13136        f.write_str(match self {
13137            MatchSearchModifier::NaturalLanguage => "IN NATURAL LANGUAGE MODE",
13138            MatchSearchModifier::NaturalLanguageQueryExpansion => {
13139                "IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION"
13140            }
13141            MatchSearchModifier::Boolean => "IN BOOLEAN MODE",
13142            MatchSearchModifier::QueryExpansion => "WITH QUERY EXPANSION",
13143        })
13144    }
13145}
13146
13147impl Render for TrimSide {
13148    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13149        f.write_str(match self {
13150            TrimSide::Both => "BOTH",
13151            TrimSide::Leading => "LEADING",
13152            TrimSide::Trailing => "TRAILING",
13153        })
13154    }
13155}
13156
13157impl<X: Extension + Render> Render for CaseExpr<X> {
13158    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13159        f.write_str("CASE")?;
13160        if let Some(operand) = &self.operand {
13161            f.write_str(" ")?;
13162            operand.render(ctx, f)?;
13163        }
13164        for clause in &self.when_clauses {
13165            f.write_str(" WHEN ")?;
13166            clause.condition.render(ctx, f)?;
13167            f.write_str(" THEN ")?;
13168            clause.result.render(ctx, f)?;
13169        }
13170        if let Some(else_result) = &self.else_result {
13171            f.write_str(" ELSE ")?;
13172            else_result.render(ctx, f)?;
13173        }
13174        f.write_str(" END")
13175    }
13176}
13177
13178impl<X: Extension + Render> Render for FunctionCall<X> {
13179    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13180        self.name.render(ctx, f)?;
13181        f.write_str("(")?;
13182        if let Some(quantifier) = &self.quantifier {
13183            quantifier.render(ctx, f)?;
13184            f.write_str(" ")?;
13185        }
13186        if self.wildcard {
13187            f.write_str("*")?;
13188        } else {
13189            render_comma_separated(&self.args, ctx, f)?;
13190        }
13191        if !self.order_by.is_empty() {
13192            // DuckDB's standalone form (`rank(ORDER BY x)`) has no positional argument, so
13193            // the `ORDER BY` opens the argument list — no separating space after `(`. A
13194            // preceding wildcard or argument list takes the space; a quantifier already
13195            // rendered its own trailing space.
13196            if self.wildcard || !self.args.is_empty() {
13197                f.write_str(" ")?;
13198            }
13199            f.write_str("ORDER BY ")?;
13200            render_comma_separated(&self.order_by, ctx, f)?;
13201        }
13202        // The MySQL `GROUP_CONCAT` delimiter rides inside the parentheses, after any
13203        // in-parenthesis `ORDER BY`, matching the source order it was parsed in.
13204        if let Some(separator) = &self.separator {
13205            f.write_str(" SEPARATOR ")?;
13206            separator.render(ctx, f)?;
13207        }
13208        // DuckDB's `IGNORE NULLS` / `RESPECT NULLS` null-treatment rides inside the
13209        // parentheses, after any in-parenthesis `ORDER BY` — the position the engine
13210        // accepts (the standard's post-`)` spelling engine-rejects there).
13211        if let Some(null_treatment) = &self.null_treatment {
13212            f.write_str(" ")?;
13213            null_treatment.render(ctx, f)?;
13214        }
13215        f.write_str(")")?;
13216        // MySQL's window-function post-`)` tail rides after the closing parenthesis and
13217        // before `OVER` (and the aggregate clauses MySQL leaves off), in the fixed
13218        // `FROM {FIRST | LAST}` then null-treatment order the grammar admits.
13219        if let Some(tail) = &self.window_tail {
13220            if let Some(from_first_last) = tail.from_first_last {
13221                f.write_str(match from_first_last {
13222                    FromFirstLast::First => " FROM FIRST",
13223                    FromFirstLast::Last => " FROM LAST",
13224                })?;
13225            }
13226            if let Some(null_treatment) = &tail.null_treatment {
13227                f.write_str(" ")?;
13228                null_treatment.render(ctx, f)?;
13229            }
13230        }
13231        // WITHIN GROUP precedes FILTER and OVER, matching PostgreSQL's
13232        // `func_application within_group_clause filter_clause over_clause` order.
13233        if let Some(within_group) = &self.within_group {
13234            f.write_str(" WITHIN GROUP (ORDER BY ")?;
13235            render_comma_separated(within_group, ctx, f)?;
13236            f.write_str(")")?;
13237        }
13238        if let Some(filter) = &self.filter {
13239            // DuckDB round-trips the keyword-less `FILTER (<predicate>)` spelling; every
13240            // other dialect wrote (and re-renders) the standard `WHERE`.
13241            match self.filter_where {
13242                FilterWhereSpelling::Where => f.write_str(" FILTER (WHERE ")?,
13243                FilterWhereSpelling::Omitted => f.write_str(" FILTER (")?,
13244            }
13245            filter.render(ctx, f)?;
13246            f.write_str(")")?;
13247        }
13248        if let Some(over) = &self.over {
13249            f.write_str(" OVER ")?;
13250            over.render(ctx, f)?;
13251        }
13252        Ok(())
13253    }
13254}
13255
13256impl<X: Extension + Render> Render for FunctionArg<X> {
13257    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13258        // The `VARIADIC` array-spread marker prefixes the whole argument, ahead of any
13259        // named-argument arrow (`VARIADIC name => value`). It is a structural keyword, so
13260        // it is emitted in both the plain and the redacted fingerprint modes.
13261        if self.variadic {
13262            f.write_str("VARIADIC ")?;
13263        }
13264        // A named argument prints its `name` and the arrow the source wrote; a
13265        // positional argument carries no name and prints just the value. The name is
13266        // an identifier, so it is masked like one for the redacted fingerprint.
13267        if let Some(name) = self.name {
13268            if ctx.mode() == RenderMode::Redacted {
13269                f.write_str("id")?;
13270            } else {
13271                f.write_str(ctx.resolve(name))?;
13272            }
13273            f.write_str(match self.syntax {
13274                ArgSyntax::ColonEquals => " := ",
13275                // A named argument is never `Positional`; the current `=>` is the
13276                // canonical spelling for the otherwise-unreachable case.
13277                ArgSyntax::Arrow | ArgSyntax::Positional => " => ",
13278            })?;
13279        }
13280        self.value.render(ctx, f)
13281    }
13282}
13283
13284impl<X: Extension + Render> Render for StructField<X> {
13285    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13286        // The key is a field name, so the redacted fingerprint masks it like an
13287        // identifier (collapsing the quote spelling, mirroring `Ident`); otherwise it
13288        // re-emits the source spelling, doubling the embedded delimiter exactly as
13289        // the lexer's doubled-close rule expects so the key round-trips.
13290        if ctx.mode() == RenderMode::Redacted {
13291            f.write_str("id")?;
13292        } else {
13293            let text = ctx.resolve(self.key);
13294            match self.key_spelling {
13295                StructKeySpelling::Bare => f.write_str(text)?,
13296                StructKeySpelling::SingleQuoted => {
13297                    if text.contains('\'') {
13298                        write!(f, "'{}'", text.replace('\'', "''"))?;
13299                    } else {
13300                        write!(f, "'{text}'")?;
13301                    }
13302                }
13303                StructKeySpelling::DoubleQuoted => {
13304                    if text.contains('"') {
13305                        write!(f, "\"{}\"", text.replace('"', "\"\""))?;
13306                    } else {
13307                        write!(f, "\"{text}\"")?;
13308                    }
13309                }
13310            }
13311        }
13312        f.write_str(": ")?;
13313        self.value.render(ctx, f)
13314    }
13315}
13316
13317impl<X: Extension + Render> Render for StructConstructorField<X> {
13318    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13319        if let Some(name) = &self.name {
13320            name.render(ctx, f)?;
13321            f.write_str(" ")?;
13322        }
13323        self.ty.render(ctx, f)
13324    }
13325}
13326
13327impl<X: Extension + Render> Render for StructConstructorArg<X> {
13328    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13329        self.value.render(ctx, f)?;
13330        if let Some(alias) = &self.alias {
13331            f.write_str(" AS ")?;
13332            alias.render(ctx, f)?;
13333        }
13334        Ok(())
13335    }
13336}
13337
13338impl<X: Extension + Render> Render for WindowSpec<X> {
13339    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13340        match self {
13341            WindowSpec::Named { name, .. } => name.render(ctx, f),
13342            WindowSpec::Inline { definition, .. } => {
13343                f.write_str("(")?;
13344                definition.render(ctx, f)?;
13345                f.write_str(")")
13346            }
13347        }
13348    }
13349}
13350
13351impl<X: Extension + Render> Render for WindowDefinition<X> {
13352    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13353        let mut wrote = false;
13354        if let Some(existing) = &self.existing {
13355            existing.render(ctx, f)?;
13356            wrote = true;
13357        }
13358        if !self.partition_by.is_empty() {
13359            if wrote {
13360                f.write_str(" ")?;
13361            }
13362            f.write_str("PARTITION BY ")?;
13363            render_comma_separated(&self.partition_by, ctx, f)?;
13364            wrote = true;
13365        }
13366        if !self.order_by.is_empty() {
13367            if wrote {
13368                f.write_str(" ")?;
13369            }
13370            f.write_str("ORDER BY ")?;
13371            render_comma_separated(&self.order_by, ctx, f)?;
13372            wrote = true;
13373        }
13374        if let Some(frame) = &self.frame {
13375            if wrote {
13376                f.write_str(" ")?;
13377            }
13378            frame.render(ctx, f)?;
13379        }
13380        Ok(())
13381    }
13382}
13383
13384impl<X: Extension + Render> Render for WindowFrame<X> {
13385    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13386        self.units.render(ctx, f)?;
13387        f.write_str(" ")?;
13388        if let Some(end) = &self.end {
13389            f.write_str("BETWEEN ")?;
13390            self.start.render(ctx, f)?;
13391            f.write_str(" AND ")?;
13392            end.render(ctx, f)?;
13393        } else {
13394            self.start.render(ctx, f)?;
13395        }
13396        if let Some(exclusion) = &self.exclusion {
13397            f.write_str(" EXCLUDE ")?;
13398            exclusion.render(ctx, f)?;
13399        }
13400        Ok(())
13401    }
13402}
13403
13404impl Render for WindowFrameUnits {
13405    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13406        f.write_str(match self {
13407            WindowFrameUnits::Rows => "ROWS",
13408            WindowFrameUnits::Range => "RANGE",
13409            WindowFrameUnits::Groups => "GROUPS",
13410        })
13411    }
13412}
13413
13414impl<X: Extension + Render> Render for WindowFrameBound<X> {
13415    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13416        match self {
13417            WindowFrameBound::CurrentRow { .. } => f.write_str("CURRENT ROW"),
13418            WindowFrameBound::UnboundedPreceding { .. } => f.write_str("UNBOUNDED PRECEDING"),
13419            WindowFrameBound::UnboundedFollowing { .. } => f.write_str("UNBOUNDED FOLLOWING"),
13420            WindowFrameBound::Preceding { offset, .. } => {
13421                offset.render(ctx, f)?;
13422                f.write_str(" PRECEDING")
13423            }
13424            WindowFrameBound::Following { offset, .. } => {
13425                offset.render(ctx, f)?;
13426                f.write_str(" FOLLOWING")
13427            }
13428        }
13429    }
13430}
13431
13432impl Render for WindowFrameExclusion {
13433    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13434        f.write_str(match self {
13435            WindowFrameExclusion::CurrentRow => "CURRENT ROW",
13436            WindowFrameExclusion::Group => "GROUP",
13437            WindowFrameExclusion::Ties => "TIES",
13438            WindowFrameExclusion::NoOthers => "NO OTHERS",
13439        })
13440    }
13441}
13442
13443impl<X: Extension + Render> Render for NamedWindow<X> {
13444    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13445        self.name.render(ctx, f)?;
13446        f.write_str(" AS (")?;
13447        self.definition.render(ctx, f)?;
13448        f.write_str(")")
13449    }
13450}
13451
13452impl<X: Extension + Render> Render for OrderByExpr<X> {
13453    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13454        self.expr.render(ctx, f)?;
13455        // `USING <operator>` (PostgreSQL) is mutually exclusive with `ASC`/`DESC`, so
13456        // `asc` is `None` here and `render_sort_direction` emits only the `NULLS`
13457        // suffix that may still follow.
13458        if let Some(using) = &self.using {
13459            f.write_str(" USING ")?;
13460            using.render(ctx, f)?;
13461        }
13462        render_sort_direction(self.asc, self.nulls_first, f)
13463    }
13464}
13465
13466impl Render for OrderByUsing {
13467    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13468        // A schema-qualified operator must round-trip through the explicit
13469        // `OPERATOR(schema.op)` spelling; a bare operator renders symbolically
13470        // (which also canonicalizes an unqualified `OPERATOR(<)` to bare `<`,
13471        // the same operator).
13472        let Some(schema) = &self.schema else {
13473            return f.write_str(ctx.resolve(self.op));
13474        };
13475        f.write_str("OPERATOR(")?;
13476        for part in &schema.0 {
13477            part.render(ctx, f)?;
13478            f.write_str(".")?;
13479        }
13480        f.write_str(ctx.resolve(self.op))?;
13481        f.write_str(")")
13482    }
13483}
13484
13485impl Render for OrderByAll {
13486    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13487        f.write_str("ALL")?;
13488        render_sort_direction(self.asc, self.nulls_first, f)
13489    }
13490}
13491
13492impl<X: Extension + Render> Render for Limit<X> {
13493    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13494        match self.syntax {
13495            // MySQL/SQLite `LIMIT <offset>, <count>`: a source-fidelity render replays
13496            // the comma spelling; a re-spell and the redacted fingerprint fall through
13497            // to the canonical `LIMIT <count> OFFSET <offset>` below.
13498            LimitSyntax::CommaOffset if honours_source_spelling(ctx) => {
13499                f.write_str("LIMIT ")?;
13500                if let Some(offset) = &self.offset {
13501                    offset.render(ctx, f)?;
13502                }
13503                f.write_str(", ")?;
13504                if let Some(limit) = &self.limit {
13505                    limit.render(ctx, f)?;
13506                }
13507                Ok(())
13508            }
13509            LimitSyntax::LimitOffset | LimitSyntax::CommaOffset => {
13510                let mut wrote = false;
13511                if let Some(limit) = &self.limit {
13512                    f.write_str("LIMIT ")?;
13513                    // DuckDB percentage limit: re-emit the written marker (`LIMIT 40 PERCENT`
13514                    // / `LIMIT 35%`). `None` is the ordinary row count. The `%` spelling
13515                    // renders with no separating space (`LIMIT 20 %` canonicalizes onto it).
13516                    // The marker reduces onto a multiplicative-or-tighter operand, so a
13517                    // count carrying a looser binary operator is re-parenthesized to reparse
13518                    // as the percentage count rather than re-associating with a `%` read as
13519                    // modulo (`LIMIT (30-10)%`, not `LIMIT 30 - 10%`) — mirroring the parse
13520                    // threshold in `parse_limit_percent_operand` (`Side::Right` of the
13521                    // multiplicative rank). A `PERCENT`-keyword count is always a literal, so
13522                    // the wrap is a no-op there.
13523                    match self.percent {
13524                        Some(marker) => {
13525                            render_pg_operand(
13526                                limit,
13527                                ctx.target().binding_powers.multiplicative,
13528                                Side::Right,
13529                                ctx,
13530                                f,
13531                            )?;
13532                            f.write_str(match marker {
13533                                LimitPercent::Symbol => "%",
13534                                LimitPercent::Keyword => " PERCENT",
13535                            })?;
13536                        }
13537                        None => limit.render(ctx, f)?,
13538                    }
13539                    wrote = true;
13540                }
13541                if let Some(offset) = &self.offset {
13542                    if wrote {
13543                        f.write_str(" ")?;
13544                    }
13545                    f.write_str("OFFSET ")?;
13546                    offset.render(ctx, f)?;
13547                }
13548                Ok(())
13549            }
13550            LimitSyntax::FetchFirst => {
13551                // `FIRST`/`NEXT` and `ROW`/`ROWS` are interchangeable surface noise: a
13552                // source-fidelity render replays the written pair (`fetch_spelling`), a
13553                // re-spell and the redacted fingerprint keep the canonical `FIRST` /
13554                // `ROWS`. The `OFFSET … ROWS` word always renders plural (its rare
13555                // singular form is not tagged — it never crossed the sweep).
13556                let fidelity = honours_source_spelling(ctx);
13557                let mut wrote = false;
13558                if let Some(offset) = &self.offset {
13559                    f.write_str("OFFSET ")?;
13560                    offset.render(ctx, f)?;
13561                    f.write_str(" ROWS")?;
13562                    wrote = true;
13563                }
13564                // `with_ties: Some(_)` is the signal that a `FETCH` tail was
13565                // actually written (see `Limit::with_ties`'s doc comment) — the
13566                // count itself is optional (`FETCH FIRST ROWS ONLY`), so `limit`
13567                // alone cannot carry that.
13568                if let Some(with_ties) = self.with_ties {
13569                    if wrote {
13570                        f.write_str(" ")?;
13571                    }
13572                    let spelling = if fidelity {
13573                        self.fetch_spelling
13574                    } else {
13575                        FetchSpelling::FirstRows
13576                    };
13577                    f.write_str(spelling.fetch_keyword())?;
13578                    if let Some(limit) = &self.limit {
13579                        f.write_str(" ")?;
13580                        limit.render(ctx, f)?;
13581                    }
13582                    f.write_str(spelling.row_word())?;
13583                    f.write_str(if with_ties { "WITH TIES" } else { "ONLY" })?;
13584                }
13585                Ok(())
13586            }
13587        }
13588    }
13589}
13590
13591impl<X: Extension + Render> Render for LimitBy<X> {
13592    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13593        f.write_str("LIMIT ")?;
13594        self.limit.render(ctx, f)?;
13595        if let Some(offset) = &self.offset {
13596            f.write_str(" OFFSET ")?;
13597            offset.render(ctx, f)?;
13598        }
13599        f.write_str(" BY ")?;
13600        render_comma_separated(&self.by, ctx, f)
13601    }
13602}
13603
13604impl<X: Extension + Render> Render for Setting<X> {
13605    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13606        self.name.render(ctx, f)?;
13607        f.write_str(" = ")?;
13608        self.value.render(ctx, f)
13609    }
13610}
13611
13612impl Render for FormatClause {
13613    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13614        f.write_str("FORMAT ")?;
13615        self.name.render(ctx, f)
13616    }
13617}
13618
13619impl Render for ForClause {
13620    /// Renders the MSSQL `FOR XML`/`FOR JSON` tail with the directives in the canonical
13621    /// MSSQL order (parse accepts them order-independently). Callers write the leading
13622    /// space.
13623    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13624        match self {
13625            ForClause::Xml {
13626                mode,
13627                elements,
13628                binary_base64,
13629                typed,
13630                root,
13631                ..
13632            } => {
13633                f.write_str("FOR XML ")?;
13634                mode.render(ctx, f)?;
13635                if *binary_base64 {
13636                    f.write_str(", BINARY BASE64")?;
13637                }
13638                if *typed {
13639                    f.write_str(", TYPE")?;
13640                }
13641                if let Some(root) = root {
13642                    f.write_str(", ")?;
13643                    root.render(ctx, f)?;
13644                }
13645                if let Some(elements) = elements {
13646                    f.write_str(", ")?;
13647                    elements.render(ctx, f)?;
13648                }
13649                Ok(())
13650            }
13651            ForClause::Json {
13652                mode,
13653                root,
13654                include_null_values,
13655                without_array_wrapper,
13656                ..
13657            } => {
13658                f.write_str("FOR JSON ")?;
13659                mode.render(ctx, f)?;
13660                if let Some(root) = root {
13661                    f.write_str(", ")?;
13662                    root.render(ctx, f)?;
13663                }
13664                if *include_null_values {
13665                    f.write_str(", INCLUDE_NULL_VALUES")?;
13666                }
13667                if *without_array_wrapper {
13668                    f.write_str(", WITHOUT_ARRAY_WRAPPER")?;
13669                }
13670                Ok(())
13671            }
13672        }
13673    }
13674}
13675
13676impl Render for ForXmlMode {
13677    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13678        match self {
13679            ForXmlMode::Raw { name, .. } => {
13680                f.write_str("RAW")?;
13681                render_optional_for_name(name.as_ref(), ctx, f)
13682            }
13683            ForXmlMode::Auto { .. } => f.write_str("AUTO"),
13684            ForXmlMode::Explicit { .. } => f.write_str("EXPLICIT"),
13685            ForXmlMode::Path { name, .. } => {
13686                f.write_str("PATH")?;
13687                render_optional_for_name(name.as_ref(), ctx, f)
13688            }
13689        }
13690    }
13691}
13692
13693impl Render for ForXmlElements {
13694    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13695        f.write_str(match self {
13696            ForXmlElements::Plain => "ELEMENTS",
13697            ForXmlElements::XsiNil => "ELEMENTS XSINIL",
13698            ForXmlElements::Absent => "ELEMENTS ABSENT",
13699        })
13700    }
13701}
13702
13703impl Render for ForJsonMode {
13704    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13705        f.write_str(match self {
13706            ForJsonMode::Auto => "AUTO",
13707            ForJsonMode::Path => "PATH",
13708        })
13709    }
13710}
13711
13712impl Render for ForRoot {
13713    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13714        f.write_str("ROOT")?;
13715        render_optional_for_name(self.name.as_ref(), ctx, f)
13716    }
13717}
13718
13719/// Render an optional `('name')` element/root name (`RAW`/`PATH`/`ROOT`); a no-op when
13720/// the name is absent.
13721fn render_optional_for_name(
13722    name: Option<&Literal>,
13723    ctx: &RenderCtx<'_>,
13724    f: &mut fmt::Formatter<'_>,
13725) -> fmt::Result {
13726    if let Some(name) = name {
13727        f.write_str("(")?;
13728        name.render(ctx, f)?;
13729        f.write_str(")")?;
13730    }
13731    Ok(())
13732}