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, ColocationPartitionKind,
33    ColumnConstraint, ColumnDef, ColumnOption, ColumnsSpelling, CommentOnStatement, CommentTarget,
34    CompoundStatement, ComprehensionSource, ConditionInfoItem, ConditionInfoItemName,
35    ConditionValue, ConditionalBranch, ConfigParameter, ConflictAction, ConflictResolution,
36    ConflictTarget, ConstraintCharacteristics, ConstraintCheckTime, ConstraintsTarget,
37    CopyDirection, CopyIntoSource, CopyIntoStatement, CopyIntoTarget, CopyOption, CopyOptionValue,
38    CopySource, CopyStatement, CopyTarget, CreateColocationGroup, CreateDatabase, CreateEvent,
39    CreateExtension, CreateExtensionOption, CreateFunction, CreateIndex, CreateLogfileGroup,
40    CreateMacro, CreateProcedure, CreateResourceGroup, CreateSchema, CreateSecret, CreateSequence,
41    CreateServer, 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, DropColocationGroup,
49    DropDatabase, DropEvent, DropIndexOnTable, DropLogfileGroup, DropObjectKind, DropResourceGroup,
50    DropSecretStmt, DropServer, DropSpatialReferenceSystem, DropStatement, DropTablespace,
51    DropTransform, 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, InsertModifier,
67    InsertOverriding, InsertSource, InsertTarget, InsertValue, InsertValues, InsertVerb,
68    InstallComponentSetElement, InstallComponentSetScope, InstallComponentSetValue,
69    InstallStatement, InstanceLockStatement, IntWidth, IntegerDivideSpelling, IntegerTypeName,
70    IntervalFields, IntoTarget, IoThreadKeyword, IsDistinctFromSpelling, IsJsonExpr,
71    IsNotDistinctFromSpelling, IsolationLevel, IterateStatement, Join, JoinConstraint,
72    JoinOperator, JsonAggregateBody, JsonAggregateExpr, JsonArrayBody, JsonArrayExpr, JsonBehavior,
73    JsonBehaviorKind, JsonConstructorExpr, JsonConstructorKind, JsonEncoding, JsonFormat,
74    JsonFuncExpr, JsonFuncKind, JsonItemType, JsonKeyValue, JsonKeyValueSpelling, JsonNullClause,
75    JsonObjectExpr, JsonPassingArg, JsonQuotesBehavior, JsonReturning, JsonTable, JsonTableColumn,
76    JsonValueExpr, JsonWrapperBehavior, KeyCacheName, KillStatement, KillTarget,
77    LambdaParamSpelling, LanguageName, LateralView, LeaveStatement, LikeSpelling, Limit, LimitBy,
78    LimitPercent, LimitSyntax, Literal, LiteralKind, LoadDataConcurrency, LoadDataDuplicate,
79    LoadDataEnclosed, LoadDataFieldOrVar, LoadDataFields, LoadDataFormat, LoadDataIgnoreRows,
80    LoadDataIgnoreUnit, LoadDataLines, LoadDataStatement, LoadFieldsSpelling, LoadIndexStatement,
81    LoadIndexTable, LoadIndexTargets, LoadStatement, LoadTarget, LockStrength, LockTablesStatement,
82    LockWait, 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, RefreshMaterializedView, RegexpSpelling,
95    ReindexStatement, RelationInheritance, RenameStatement, RepairTableOption, RepeatStatement,
96    RepetitionQuantifier, ReplicaSpelling, ReplicaThreadOption, ReplicaUntilCondition,
97    ReplicationFilterRule, ReplicationStatement, RequirePrimaryKeyCheck, ResourceGroupState,
98    ResourceGroupThreadPriority, ResourceGroupType, ResourceGroupVcpu, ResourceLimit,
99    ReturnStatement, Returning, RewriteDbPair, RoleSpec, RollupSpelling, RoutineKind,
100    RoutineObjectKind, RoutineSignature, RowsFromItem, RowsPerMatch, SampleClause, SampleUnit,
101    SchemaObjectKind, SchemaRelocationObject, SecretOption, SecretPersistence, Select,
102    SelectDistinct, SelectItem, SelectSpelling, SemiAntiSide, SemiStructuredAccessExpr,
103    SemiStructuredPathSegment, ServerOption, ServerOptionKind, SessionStatement,
104    SessionVariableKind, SetAssignment, SetCharacterSetValue, SetExpr, SetNamesValue, SetOperator,
105    SetParameterValue, SetQuantifier, SetScope, SetValue, SetVariableAssignment,
106    SetVariableKeyword, SetVariableValue, Setting, ShowBare, ShowColumnsSpelling, ShowCreateKind,
107    ShowDiagnosticKind, ShowEngineArtifact, ShowFilter, ShowFrom, ShowFromKeyword,
108    ShowFunctionsFilter, ShowFunctionsScope, ShowIndexSpelling, ShowLimit, ShowListing,
109    ShowProfileType, ShowRef, ShowRefKind, ShowRefTarget, ShowRoutineKind, ShowScope,
110    ShowStatement, ShowTarget, SignalItem, SignalItemName, SignalStatement, Signedness,
111    SizeLiteral, SizeUnit, SpecialFunctionKeyword, SpecialSetValue, SqlDataAccess,
112    SqlSecurityContext, SrsAttribute, Statement, StatementInfoItem, StatementInfoItemName,
113    StringFunc, StructConstructorArg, StructConstructorField, StructField, StructKeySpelling,
114    StructTypeField, StructTypeSpelling, SubscriptKind, SubsetDefinition, SymbolDefinition,
115    SystemVariableScope, SystemVariableScopeKind, TableAlias, TableConstraint, TableConstraintDef,
116    TableElement, TableFactor, TableFunctionColumn, TableHint, TableKeyword, TableLikeAction,
117    TableLikeFeature, TableLikeOption, TableLock, TableLockKind, TableMaintenanceKind,
118    TableMaintenanceStatement, TableOption, TableOptionValue, TableRename, TableSample,
119    TableStorageParameter, TableVersion, TableWithJoins, TablespaceOption, TablespaceSizeOption,
120    TemporaryTableKind, TextTypeName, TimeTypeName, TimeZone, TimestampTypeName, TlsOption,
121    TlsRequirement, TransactionAccessMode, TransactionBlockKeyword, TransactionCommitKeyword,
122    TransactionMode, TransactionModeKind, TransactionRollbackKeyword, TransactionStart,
123    TransactionStatement, TriggerEvent, TriggerOrder, TriggerTiming, TrimSide, TruthValue,
124    UnaryOperator, UndoTablespaceState, UninstallStatement, UnlockTablesStatement, Unpivot,
125    UnpivotColumn, UnpivotSpelling, Update, UpdateAssignment, UpdateExtensionsStatement,
126    UpdateTupleSource, UpdateValue, Upsert, UseStatement, UserAttribute, UserRename, UserRoleList,
127    UserRoleListKind, UserSpec, VacuumAnalyze, VacuumStatement, Values, ValuesItem, VcpuRange,
128    ViewAlgorithm, ViewCheckOption, ViewOptions, WhileStatement, WildcardOptions, WildcardRename,
129    WildcardReplace, WindowDefinition, WindowFrame, WindowFrameBound, WindowFrameExclusion,
130    WindowFrameUnits, WindowSpec, With, WithRoleSpec, WrappedTypeKind, XaAssociation,
131    XaStartKeyword, XaStatement, XaSuspend, Xid, XmlAttribute, XmlDocumentOrContent, XmlFunc,
132    XmlIndentOption, XmlNamespace, XmlPassingMechanism, XmlStandalone, XmlTable, XmlTableColumn,
133    XmlWhitespaceOption,
134};
135use crate::dialect::TargetSpelling;
136use crate::precedence::{
137    BindingPower, BindingPowerTable, SetOperationBindingPowerTable, Side, UNPARENTHESIZED_IN_LIST,
138    needs_parens_between,
139};
140use std::fmt;
141use thin_vec::ThinVec;
142
143// The render-shape fingerprint pins (ADR-0013), one per AST source file
144// (`ast/<family>.rs`). Each `const _` compiles only while its `<0x…>` still equals
145// the matching `CURRENT_RENDER_SHAPE_<FAMILY>` hash sourcegen last wrote for that
146// file's slice of the AST shape. Change a shape in `ast/<family>.rs` (add, drop, or
147// retype a field or variant) and sourcegen regenerates a new
148// `RenderShapeFingerprint<0x…>` for THAT family only, so only that family's pin
149// below stops compiling with an `expected RenderShapeFingerprint<…>, found
150// RenderShapeFingerprint<…>` mismatch (the two const-generic fingerprints differ) —
151// the alarm that the hand-written impls below may no longer cover every field or
152// variant of that file's nodes. Do NOT just paste the new hash in to silence it:
153// that skips the audit this pin exists to force. Fix it in order (the procedure below):
154//   1. rerun `cargo run -p squonk-sourcegen` so `generated/render_skeleton.rs`
155//      reflects the new shape;
156//   2. audit the hand-written `Render` impls for that family's nodes (the impls are
157//      interleaved through this file — the const name, not a line range, is the
158//      index) against the regenerated `render_skeleton`, updating the render text;
159//   3. THEN copy the regenerated `CURRENT_RENDER_SHAPE_<FAMILY>` hash into the
160//      matching `<0x…>` below — leave every other family's pin untouched.
161// The compile error can only carry the two hashes, not this procedure, so it lives
162// here.
163//
164// Why per-family (ADR-0013 split): a shape change moves ONE pin line, so two agents
165// editing disjoint files (`expr.rs` vs `ddl.rs`) touch disjoint pins and merge with
166// no conflict, while two agents landing shapes in the SAME file still collide loudly
167// on that file's single shared pin. The pins and the `render_skeleton` module they
168// reference are all `#[cfg(test)]`-gated, so product builds don't compile the
169// thousands of dead skeleton lines; the drift alarm surfaces under `cargo nextest
170// run` (test builds), not `cargo build -p squonk-ast`.
171#[cfg(test)]
172use crate::generated::render_skeleton as skeleton;
173#[cfg(test)]
174const _: skeleton::RenderShapeFingerprint<0x0bc66b9b4759b4e4> = skeleton::CURRENT_RENDER_SHAPE_DCL;
175#[cfg(test)]
176const _: skeleton::RenderShapeFingerprint<0xb7fb9938befad896> = skeleton::CURRENT_RENDER_SHAPE_DDL;
177#[cfg(test)]
178const _: skeleton::RenderShapeFingerprint<0x5bdcbbc8cc864aec> = skeleton::CURRENT_RENDER_SHAPE_DML;
179#[cfg(test)]
180const _: skeleton::RenderShapeFingerprint<0xde8cec2513ed42e5> = skeleton::CURRENT_RENDER_SHAPE_EXPR;
181#[cfg(test)]
182const _: skeleton::RenderShapeFingerprint<0xffd1225e92e4dab2> = skeleton::CURRENT_RENDER_SHAPE_EXT;
183#[cfg(test)]
184const _: skeleton::RenderShapeFingerprint<0xbc5427055a4e813c> =
185    skeleton::CURRENT_RENDER_SHAPE_IDENT;
186#[cfg(test)]
187const _: skeleton::RenderShapeFingerprint<0x509ea17dec9af986> =
188    skeleton::CURRENT_RENDER_SHAPE_LITERAL;
189#[cfg(test)]
190const _: skeleton::RenderShapeFingerprint<0x8b1ed704455adf27> =
191    skeleton::CURRENT_RENDER_SHAPE_MATCH_RECOGNIZE;
192#[cfg(test)]
193const _: skeleton::RenderShapeFingerprint<0x5ebfaa9fb0c1caf9> =
194    skeleton::CURRENT_RENDER_SHAPE_PIPE_OPS;
195#[cfg(test)]
196const _: skeleton::RenderShapeFingerprint<0xd3444362d3f03fe8> =
197    skeleton::CURRENT_RENDER_SHAPE_PIVOT;
198#[cfg(test)]
199const _: skeleton::RenderShapeFingerprint<0xe6cc8d2df67c101c> =
200    skeleton::CURRENT_RENDER_SHAPE_QUERY;
201#[cfg(test)]
202const _: skeleton::RenderShapeFingerprint<0x957db431c4a1db51> = skeleton::CURRENT_RENDER_SHAPE_STMT;
203#[cfg(test)]
204const _: skeleton::RenderShapeFingerprint<0xfb50d22f049b4cc7> =
205    skeleton::CURRENT_RENDER_SHAPE_STORED_PROGRAM;
206#[cfg(test)]
207const _: skeleton::RenderShapeFingerprint<0x1a58662f47080916> = skeleton::CURRENT_RENDER_SHAPE_TCL;
208#[cfg(test)]
209const _: skeleton::RenderShapeFingerprint<0x03efd7c0644eb26a> = skeleton::CURRENT_RENDER_SHAPE_TY;
210#[cfg(test)]
211const _: skeleton::RenderShapeFingerprint<0x30c6b7a3fb94d049> = skeleton::CURRENT_RENDER_SHAPE_UTIL;
212#[cfg(test)]
213const _: skeleton::RenderShapeFingerprint<0x585b3bd313272236> =
214    skeleton::CURRENT_RENDER_SHAPE_WINDOW;
215
216// ---------------------------------------------------------------------------
217// Leaf vocabulary
218// ---------------------------------------------------------------------------
219
220impl Render for NoExt {
221    fn render(&self, _ctx: &RenderCtx<'_>, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        // `NoExt` is uninhabited: the stock AST has no extension node to render.
223        match *self {}
224    }
225}
226
227impl Render for Ident {
228    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        // Redacted rendering masks every identifier to a fixed placeholder so no
230        // original name reaches a query fingerprint or a PII-free log (ADR-0010),
231        // mirroring how `Literal` masks values to `?`. Emitting the mask before the
232        // symbol is resolved keeps the source spelling out of the output for every
233        // quote style and for a keyword used as an identifier; `ObjectName` renders
234        // one placeholder per dotted part, so qualified-name arity survives for
235        // query-shape discrimination while the names themselves do not.
236        if ctx.mode() == RenderMode::Redacted {
237            return f.write_str("id");
238        }
239        let text = ctx.resolve(self.sym);
240        // Doubling the close delimiter escapes an embedded copy, mirroring the
241        // lexer's doubled-close rule so the identifier round-trips. Bracket quoting
242        // is asymmetric, so only the close `]` doubles, never the open `[`.
243        let (open, close, doubled) = match self.quote {
244            QuoteStyle::None => return f.write_str(text),
245            // A `U&"…"` Unicode-escaped identifier's decoded value (`sym`) differs from its
246            // source spelling. Under a source-fidelity render the exact `U&"…" [UESCAPE 'c']`
247            // slice replays verbatim; a `TargetDialect` re-spell, the redacted fingerprint,
248            // or a detached node with no backing source fall through to the plain
249            // double-quoted decoded form — semantically identical, and the only spelling a
250            // non-`unicode_strings` target can carry.
251            QuoteStyle::UnicodeDouble => {
252                if honours_source_spelling(ctx) {
253                    if let Some(src) = ctx.slice(self.meta.span) {
254                        return f.write_str(src);
255                    }
256                }
257                ('"', '"', "\"\"")
258            }
259            QuoteStyle::Single => ('\'', '\'', "''"),
260            QuoteStyle::Double => ('"', '"', "\"\""),
261            QuoteStyle::Backtick => ('`', '`', "``"),
262            QuoteStyle::Bracket => ('[', ']', "]]"),
263        };
264        // The common identifier embeds no delimiter, so keep that path borrow-only
265        // and allocate the escaped copy only when one is actually present.
266        if text.contains(close) {
267            let escaped = text.replace(close, doubled);
268            write!(f, "{open}{escaped}{close}")
269        } else {
270            write!(f, "{open}{text}{close}")
271        }
272    }
273}
274
275impl Render for ObjectName {
276    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277        for (i, part) in self.0.iter().enumerate() {
278            if i > 0 {
279                f.write_str(".")?;
280            }
281            part.render(ctx, f)?;
282        }
283        Ok(())
284    }
285}
286
287impl Render for Literal {
288    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289        if ctx.mode() == RenderMode::Redacted {
290            return f.write_str("?");
291        }
292        if let Some(text) = ctx.slice(self.meta.span) {
293            // Exact source spelling round-trips (hex/scientific/quote style).
294            return f.write_str(text);
295        }
296        // A synthesized or detached literal has no backing source; fall back to a
297        // kind-based spelling so rendering stays total instead of panicking. The
298        // temporal forms re-emit a placeholder value of the right type, carrying the
299        // time-zone flag / interval qualifier the kind tag records.
300        match &self.kind {
301            LiteralKind::Null => f.write_str("NULL"),
302            LiteralKind::Boolean(true) => f.write_str("TRUE"),
303            LiteralKind::Boolean(false) => f.write_str("FALSE"),
304            LiteralKind::Integer | LiteralKind::Float | LiteralKind::Decimal => f.write_str("0"),
305            LiteralKind::String => f.write_str("''"),
306            LiteralKind::Date => f.write_str("DATE '1970-01-01'"),
307            LiteralKind::Time { time_zone } => {
308                f.write_str("TIME")?;
309                render_time_zone_suffix(*time_zone, f)?;
310                f.write_str(" '00:00:00'")
311            }
312            LiteralKind::Timestamp { time_zone } => {
313                f.write_str("TIMESTAMP")?;
314                render_time_zone_suffix(*time_zone, f)?;
315                f.write_str(" '1970-01-01 00:00:00'")
316            }
317            LiteralKind::Interval { fields, .. } => {
318                f.write_str("INTERVAL '0'")?;
319                match fields {
320                    Some(fields) => f.write_str(interval_fields_suffix(*fields)),
321                    None => Ok(()),
322                }
323            }
324            LiteralKind::BitString { radix } => f.write_str(match radix {
325                BitStringRadix::Binary => "B'0'",
326                BitStringRadix::Hex => "X'0'",
327            }),
328            LiteralKind::Money => f.write_str("$0"),
329        }
330    }
331}
332
333impl<X: Extension + Render> Render for DataType<X> {
334    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335        let target = type_render_target(ctx);
336        match self {
337            DataType::Boolean { spelling, .. } => f.write_str(boolean_type_name(target, *spelling)),
338            DataType::TinyInt { display_width, .. } => render_sized(f, "TINYINT", *display_width),
339            DataType::SmallInt { display_width, .. } => render_sized(f, "SMALLINT", *display_width),
340            DataType::MediumInt { display_width, .. } => {
341                render_sized(f, "MEDIUMINT", *display_width)
342            }
343            DataType::Integer {
344                spelling,
345                display_width,
346                ..
347            } => render_sized(f, integer_type_name(target, *spelling), *display_width),
348            DataType::BigInt { display_width, .. } => render_sized(f, "BIGINT", *display_width),
349            DataType::Decimal {
350                spelling,
351                precision,
352                scale,
353                ..
354            } => {
355                render_precision_scale(f, decimal_type_name(target, *spelling), *precision, *scale)
356            }
357            DataType::Float { precision, .. } => render_sized(f, "FLOAT", *precision),
358            DataType::Real { .. } => f.write_str("REAL"),
359            DataType::Double { spelling, .. } => f.write_str(double_type_name(*spelling)),
360            DataType::Text {
361                spelling, charset, ..
362            } => {
363                f.write_str(text_type_name(target, *spelling))?;
364                render_optional_charset_annotation(ctx, target, charset.as_deref(), f)
365            }
366            DataType::Blob { spelling, .. } => f.write_str(blob_type_name(target, *spelling)),
367            DataType::Character {
368                spelling,
369                size,
370                charset,
371                ..
372            } => {
373                render_sized(f, character_type_name(target, *spelling), *size)?;
374                render_optional_charset_annotation(ctx, target, charset.as_deref(), f)
375            }
376            DataType::Binary { spelling, size, .. } => {
377                render_sized(f, binary_type_name(target, *spelling), *size)
378            }
379            DataType::Bit { varying, size, .. } => {
380                render_sized(f, if *varying { "BIT VARYING" } else { "BIT" }, *size)
381            }
382            DataType::Json { .. } => f.write_str("JSON"),
383            DataType::Uuid { .. } => f.write_str("UUID"),
384            DataType::Date { .. } => f.write_str("DATE"),
385            DataType::Time {
386                spelling,
387                precision,
388                time_zone,
389                ..
390            } => render_time_type(target, *spelling, *precision, *time_zone, f),
391            DataType::Timestamp {
392                spelling,
393                precision,
394                time_zone,
395                ..
396            } => render_timestamp_type(target, *spelling, *precision, *time_zone, f),
397            DataType::Interval {
398                fields, precision, ..
399            } => render_interval_type(*fields, *precision, f),
400            DataType::Enum {
401                values, charset, ..
402            } => {
403                render_value_list_type(ctx, "ENUM", values, f)?;
404                render_optional_charset_annotation(ctx, target, charset.as_deref(), f)
405            }
406            DataType::Set {
407                values, charset, ..
408            } => {
409                render_value_list_type(ctx, "SET", values, f)?;
410                render_optional_charset_annotation(ctx, target, charset.as_deref(), f)
411            }
412            DataType::NumericModifier {
413                element,
414                signedness,
415                zerofill,
416                ..
417            } => render_numeric_modifier_type(ctx, element.as_deref(), *signedness, *zerofill, f),
418            DataType::Array {
419                element,
420                size,
421                spelling,
422                ..
423            } => render_array_type(ctx, element, *size, *spelling, f),
424            DataType::Struct {
425                fields, spelling, ..
426            } => match spelling {
427                StructTypeSpelling::AngleBracket => {
428                    render_composite_type_angle(ctx, "STRUCT", fields, f)
429                }
430                _ => render_composite_type(ctx, struct_type_keyword(*spelling), fields, f),
431            },
432            DataType::Union { members, .. } => render_composite_type(ctx, "UNION", members, f),
433            DataType::Map { key, value, .. } => {
434                f.write_str("MAP(")?;
435                key.render(ctx, f)?;
436                f.write_str(", ")?;
437                value.render(ctx, f)?;
438                f.write_str(")")
439            }
440            DataType::Wrapped { kind, inner, .. } => {
441                f.write_str(wrapped_type_keyword(*kind))?;
442                f.write_str("(")?;
443                inner.render(ctx, f)?;
444                f.write_str(")")
445            }
446            // ClickHouse's case-sensitive mixed-case spelling round-trips (`FixedString`,
447            // never `FIXEDSTRING`), the same canonical casing as `wrapped_type_keyword`.
448            DataType::FixedString { length, .. } => write!(f, "FixedString({length})"),
449            // ClickHouse's mixed-case spelling round-trips (`DateTime64`, never
450            // `DATETIME64`); the optional timezone re-emits its source-spelled string literal.
451            DataType::DateTime64 {
452                precision,
453                timezone,
454                ..
455            } => {
456                write!(f, "DateTime64({precision}")?;
457                if let Some(timezone) = timezone {
458                    f.write_str(", ")?;
459                    timezone.render(ctx, f)?;
460                }
461                f.write_str(")")
462            }
463            // ClickHouse's mixed-case spelling round-trips (`Nested`, never `NESTED`); the
464            // named-field list reuses the composite renderer, distinct from `STRUCT`/`UNION`
465            // only in its keyword.
466            DataType::Nested { fields, .. } => render_composite_type(ctx, "Nested", fields, f),
467            // ClickHouse's mixed-case bit-width spelling round-trips (`Int256`/`UInt256`,
468            // never `INT256`), the same canonical casing as the wrapper keywords.
469            DataType::FixedWidthInt { signed, width, .. } => {
470                f.write_str(fixed_width_int_name(*signed, *width))
471            }
472            DataType::UserDefined {
473                name, modifiers, ..
474            } => {
475                name.render(ctx, f)?;
476                render_literal_modifiers(ctx, f, modifiers)
477            }
478            // SQLite's liberal affinity name: its words re-rendered space-separated
479            // (preserving each word's source case and quote style), then the optional
480            // one-or-two-argument modifier list, so `LONG INTEGER` / `VARCHAR(123,456)`
481            // round-trip token-for-token.
482            DataType::Liberal { words, args, .. } => {
483                for (index, word) in words.iter().enumerate() {
484                    if index > 0 {
485                        f.write_str(" ")?;
486                    }
487                    word.render(ctx, f)?;
488                }
489                render_numeric_modifiers(f, args)
490            }
491            // The `Other(X)` seam delegates to the host node's own `Render`, exactly as
492            // `Expr::Other` / `Statement::Other` do; uninhabited under `NoExt`.
493            DataType::Other { ext, .. } => ext.render(ctx, f),
494        }
495    }
496}
497
498impl<X: Extension + Render> Render for StructTypeField<X> {
499    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500        self.name.render(ctx, f)?;
501        f.write_str(" ")?;
502        self.ty.render(ctx, f)
503    }
504}
505
506/// The canonical type-name spelling family a `DataType` render targets.
507///
508/// Derived from the render [`RenderSpelling`] mode and — for a target-dialect render —
509/// the target [`FeatureSet`](crate::dialect::FeatureSet)'s
510/// [`TargetSpelling`] data, so the renderer reads the
511/// PostgreSQL-vs-ANSI choice from a field rather than recognizing a preset by identity
512/// (no `postgres` feature gate). `PreserveSource` keeps the AST's own syntax tag; the
513/// dialect families select the per-construct spelling tables below.
514#[derive(Clone, Copy, Debug, PartialEq, Eq)]
515enum TypeRenderTarget {
516    PreserveSource,
517    Ansi,
518    Postgres,
519}
520
521fn type_render_target(ctx: &RenderCtx<'_>) -> TypeRenderTarget {
522    // The target's `TargetSpelling` is the data-driven contract for which canonical
523    // spellings a target-dialect render emits; `PreserveSource` ignores the target and
524    // keeps the AST's own syntax tag (ADR-0011).
525    match ctx.spelling() {
526        RenderSpelling::PreserveSource => TypeRenderTarget::PreserveSource,
527        RenderSpelling::TargetDialect => match ctx.target().target_spelling {
528            TargetSpelling::Ansi => TypeRenderTarget::Ansi,
529            TargetSpelling::Postgres => TypeRenderTarget::Postgres,
530        },
531    }
532}
533
534fn boolean_type_name(target: TypeRenderTarget, spelling: BooleanTypeName) -> &'static str {
535    match target {
536        TypeRenderTarget::PreserveSource => match spelling {
537            BooleanTypeName::Boolean => "BOOLEAN",
538            BooleanTypeName::Bool => "BOOL",
539        },
540        TypeRenderTarget::Ansi => "BOOLEAN",
541        TypeRenderTarget::Postgres => "BOOLEAN",
542    }
543}
544
545fn integer_type_name(target: TypeRenderTarget, spelling: IntegerTypeName) -> &'static str {
546    match target {
547        TypeRenderTarget::PreserveSource => match spelling {
548            IntegerTypeName::Int => "INT",
549            IntegerTypeName::Integer => "INTEGER",
550        },
551        TypeRenderTarget::Ansi => "INTEGER",
552        TypeRenderTarget::Postgres => "INTEGER",
553    }
554}
555
556fn decimal_type_name(target: TypeRenderTarget, spelling: DecimalTypeName) -> &'static str {
557    match target {
558        TypeRenderTarget::PreserveSource => match spelling {
559            DecimalTypeName::Decimal => "DECIMAL",
560            DecimalTypeName::Dec => "DEC",
561            DecimalTypeName::Numeric => "NUMERIC",
562        },
563        TypeRenderTarget::Ansi => "DECIMAL",
564        TypeRenderTarget::Postgres => "NUMERIC",
565    }
566}
567
568fn double_type_name(spelling: DoubleTypeName) -> &'static str {
569    match spelling {
570        DoubleTypeName::DoublePrecision => "DOUBLE PRECISION",
571        DoubleTypeName::Double => "DOUBLE",
572    }
573}
574
575fn text_type_name(target: TypeRenderTarget, spelling: TextTypeName) -> &'static str {
576    match target {
577        TypeRenderTarget::PreserveSource => match spelling {
578            TextTypeName::Text => "TEXT",
579            TextTypeName::TinyText => "TINYTEXT",
580            TextTypeName::MediumText => "MEDIUMTEXT",
581            TextTypeName::LongText => "LONGTEXT",
582        },
583        // The MySQL size family has no standard spelling; collapse it to the portable
584        // `TEXT` when targeting another dialect.
585        TypeRenderTarget::Ansi | TypeRenderTarget::Postgres => "TEXT",
586    }
587}
588
589fn blob_type_name(target: TypeRenderTarget, spelling: BlobTypeName) -> &'static str {
590    match target {
591        TypeRenderTarget::PreserveSource => match spelling {
592            BlobTypeName::Blob => "BLOB",
593            BlobTypeName::TinyBlob => "TINYBLOB",
594            BlobTypeName::MediumBlob => "MEDIUMBLOB",
595            BlobTypeName::LongBlob => "LONGBLOB",
596        },
597        // No standard binary-LOB spelling; keep `BLOB` for ANSI and map to the
598        // PostgreSQL binary type for a PostgreSQL target.
599        TypeRenderTarget::Ansi => "BLOB",
600        TypeRenderTarget::Postgres => "BYTEA",
601    }
602}
603
604fn character_type_name(target: TypeRenderTarget, spelling: CharacterTypeName) -> &'static str {
605    // Whether this spelling is a `VARYING` (variable-length) character type, used
606    // to map the national-character spellings onto a target's varying/fixed form.
607    let varying = matches!(
608        spelling,
609        CharacterTypeName::CharVarying
610            | CharacterTypeName::CharacterVarying
611            | CharacterTypeName::Varchar
612            | CharacterTypeName::NcharVarying
613            | CharacterTypeName::NationalCharVarying
614            | CharacterTypeName::NationalCharacterVarying
615    );
616    match target {
617        TypeRenderTarget::PreserveSource => match spelling {
618            CharacterTypeName::Char => "CHAR",
619            CharacterTypeName::Character => "CHARACTER",
620            CharacterTypeName::CharVarying => "CHAR VARYING",
621            CharacterTypeName::CharacterVarying => "CHARACTER VARYING",
622            CharacterTypeName::Varchar => "VARCHAR",
623            CharacterTypeName::Nchar => "NCHAR",
624            CharacterTypeName::NcharVarying => "NCHAR VARYING",
625            CharacterTypeName::NationalChar => "NATIONAL CHAR",
626            CharacterTypeName::NationalCharVarying => "NATIONAL CHAR VARYING",
627            CharacterTypeName::NationalCharacter => "NATIONAL CHARACTER",
628            CharacterTypeName::NationalCharacterVarying => "NATIONAL CHARACTER VARYING",
629        },
630        TypeRenderTarget::Ansi => {
631            if varying {
632                "CHARACTER VARYING"
633            } else {
634                "CHARACTER"
635            }
636        }
637        TypeRenderTarget::Postgres => {
638            if varying {
639                "VARCHAR"
640            } else {
641                "CHAR"
642            }
643        }
644    }
645}
646
647/// Render a MySQL character-set type annotation ([`CharsetAnnotation`]) in its canonical
648/// order — the charset selector first, then `BINARY` — with a leading space to follow the
649/// type. MySQL's reversed spellings (`BINARY CHARACTER SET x`, `BINARY ASCII`) and the
650/// `CHARSET` synonym fold onto this canonical form (an ADR-0011 spelling trade; the exact
651/// written order stays recoverable from the node span).
652///
653/// The annotation has no portable spelling, so it renders only for a source-preserving
654/// round-trip; a target-dialect render drops it (like the TEXT-family size collapse).
655/// `PreserveSource` is the only target that can carry one — the field is `None` for every
656/// non-MySQL dialect.
657fn render_optional_charset_annotation(
658    ctx: &RenderCtx<'_>,
659    target: TypeRenderTarget,
660    annotation: Option<&CharsetAnnotation>,
661    f: &mut fmt::Formatter<'_>,
662) -> fmt::Result {
663    let Some(annotation) = annotation else {
664        return Ok(());
665    };
666    if target != TypeRenderTarget::PreserveSource {
667        return Ok(());
668    }
669    match annotation.charset {
670        Some(Charset::Named) => {
671            f.write_str(" CHARACTER SET ")?;
672            // `name` is `Some` for the `Named` selector (parser invariant); a detached node
673            // with a missing name renders the bare keyword rather than panicking.
674            if let Some(name) = &annotation.name {
675                name.render(ctx, f)?;
676            }
677        }
678        Some(Charset::Ascii) => f.write_str(" ASCII")?,
679        Some(Charset::Unicode) => f.write_str(" UNICODE")?,
680        Some(Charset::Byte) => f.write_str(" BYTE")?,
681        None => {}
682    }
683    if annotation.binary {
684        f.write_str(" BINARY")?;
685    }
686    Ok(())
687}
688
689fn binary_type_name(target: TypeRenderTarget, spelling: BinaryTypeName) -> &'static str {
690    match target {
691        TypeRenderTarget::PreserveSource => match spelling {
692            BinaryTypeName::Binary => "BINARY",
693            BinaryTypeName::BinaryVarying => "BINARY VARYING",
694            BinaryTypeName::Varbinary => "VARBINARY",
695            BinaryTypeName::Bytea => "BYTEA",
696        },
697        TypeRenderTarget::Ansi => match spelling {
698            BinaryTypeName::Binary => "BINARY",
699            BinaryTypeName::BinaryVarying => "BINARY VARYING",
700            BinaryTypeName::Varbinary => "BINARY VARYING",
701            BinaryTypeName::Bytea => "BYTEA",
702        },
703        TypeRenderTarget::Postgres => match spelling {
704            BinaryTypeName::Binary => "BYTEA",
705            BinaryTypeName::BinaryVarying => "BYTEA",
706            BinaryTypeName::Varbinary => "BYTEA",
707            BinaryTypeName::Bytea => "BYTEA",
708        },
709    }
710}
711
712fn render_time_type(
713    target: TypeRenderTarget,
714    spelling: TimeTypeName,
715    precision: Option<u32>,
716    time_zone: TimeZone,
717    f: &mut fmt::Formatter<'_>,
718) -> fmt::Result {
719    match target {
720        TypeRenderTarget::PreserveSource => match spelling {
721            TimeTypeName::Time => {
722                render_sized(f, "TIME", precision)?;
723                render_time_zone_suffix(time_zone, f)
724            }
725            TimeTypeName::Timetz => render_sized(f, "TIMETZ", precision),
726        },
727        TypeRenderTarget::Ansi => {
728            render_sized(f, "TIME", precision)?;
729            render_time_zone_suffix(time_zone_from_spelling(time_zone, spelling), f)
730        }
731        TypeRenderTarget::Postgres => match time_zone_from_spelling(time_zone, spelling) {
732            TimeZone::Unspecified => render_sized(f, "TIME", precision),
733            TimeZone::WithTimeZone => render_sized(f, "TIMETZ", precision),
734            TimeZone::WithoutTimeZone => {
735                render_sized(f, "TIME", precision)?;
736                f.write_str(" WITHOUT TIME ZONE")
737            }
738        },
739    }
740}
741
742fn render_timestamp_type(
743    target: TypeRenderTarget,
744    spelling: TimestampTypeName,
745    precision: Option<u32>,
746    time_zone: TimeZone,
747    f: &mut fmt::Formatter<'_>,
748) -> fmt::Result {
749    match target {
750        TypeRenderTarget::PreserveSource => match spelling {
751            TimestampTypeName::Timestamp => {
752                render_sized(f, "TIMESTAMP", precision)?;
753                render_time_zone_suffix(time_zone, f)
754            }
755            TimestampTypeName::Timestamptz => render_sized(f, "TIMESTAMPTZ", precision),
756            TimestampTypeName::Datetime => render_sized(f, "DATETIME", precision),
757        },
758        TypeRenderTarget::Ansi => {
759            render_sized(f, "TIMESTAMP", precision)?;
760            render_time_zone_suffix(time_zone_from_timestamp_spelling(time_zone, spelling), f)
761        }
762        TypeRenderTarget::Postgres => {
763            match time_zone_from_timestamp_spelling(time_zone, spelling) {
764                TimeZone::Unspecified => render_sized(f, "TIMESTAMP", precision),
765                TimeZone::WithTimeZone => render_sized(f, "TIMESTAMPTZ", precision),
766                TimeZone::WithoutTimeZone => {
767                    render_sized(f, "TIMESTAMP", precision)?;
768                    f.write_str(" WITHOUT TIME ZONE")
769                }
770            }
771        }
772    }
773}
774
775fn time_zone_from_spelling(time_zone: TimeZone, spelling: TimeTypeName) -> TimeZone {
776    match spelling {
777        TimeTypeName::Time => time_zone,
778        TimeTypeName::Timetz => TimeZone::WithTimeZone,
779    }
780}
781
782fn time_zone_from_timestamp_spelling(time_zone: TimeZone, spelling: TimestampTypeName) -> TimeZone {
783    match spelling {
784        TimestampTypeName::Timestamp => time_zone,
785        TimestampTypeName::Timestamptz => TimeZone::WithTimeZone,
786        // MySQL `DATETIME` carries no zone, so a non-PreserveSource target renders it
787        // as a plain `TIMESTAMP` (its closest portable form).
788        TimestampTypeName::Datetime => TimeZone::Unspecified,
789    }
790}
791
792fn render_time_zone_suffix(time_zone: TimeZone, f: &mut fmt::Formatter<'_>) -> fmt::Result {
793    match time_zone {
794        TimeZone::Unspecified => Ok(()),
795        TimeZone::WithTimeZone => f.write_str(" WITH TIME ZONE"),
796        TimeZone::WithoutTimeZone => f.write_str(" WITHOUT TIME ZONE"),
797    }
798}
799
800fn render_interval_type(
801    fields: Option<IntervalFields>,
802    precision: Option<u32>,
803    f: &mut fmt::Formatter<'_>,
804) -> fmt::Result {
805    f.write_str("INTERVAL")?;
806    match fields {
807        Some(IntervalFields::Year) => render_interval_field(f, "YEAR", precision),
808        Some(IntervalFields::Month) => render_interval_field(f, "MONTH", precision),
809        Some(IntervalFields::Day) => render_interval_field(f, "DAY", precision),
810        Some(IntervalFields::Hour) => render_interval_field(f, "HOUR", precision),
811        Some(IntervalFields::Minute) => render_interval_field(f, "MINUTE", precision),
812        Some(IntervalFields::Second) => render_interval_field(f, "SECOND", precision),
813        Some(IntervalFields::YearToMonth) => f.write_str(" YEAR TO MONTH"),
814        Some(IntervalFields::DayToHour) => f.write_str(" DAY TO HOUR"),
815        Some(IntervalFields::DayToMinute) => f.write_str(" DAY TO MINUTE"),
816        Some(IntervalFields::DayToSecond) => {
817            f.write_str(" DAY TO ")?;
818            render_sized(f, "SECOND", precision)
819        }
820        Some(IntervalFields::HourToMinute) => f.write_str(" HOUR TO MINUTE"),
821        Some(IntervalFields::HourToSecond) => {
822            f.write_str(" HOUR TO ")?;
823            render_sized(f, "SECOND", precision)
824        }
825        Some(IntervalFields::MinuteToSecond) => {
826            f.write_str(" MINUTE TO ")?;
827            render_sized(f, "SECOND", precision)
828        }
829        Some(IntervalFields::Week) => render_interval_field(f, "WEEK", precision),
830        Some(IntervalFields::Quarter) => render_interval_field(f, "QUARTER", precision),
831        Some(IntervalFields::Decade) => render_interval_field(f, "DECADE", precision),
832        Some(IntervalFields::Century) => render_interval_field(f, "CENTURY", precision),
833        Some(IntervalFields::Millennium) => render_interval_field(f, "MILLENNIUM", precision),
834        Some(IntervalFields::Millisecond) => render_interval_field(f, "MILLISECOND", precision),
835        Some(IntervalFields::Microsecond) => render_interval_field(f, "MICROSECOND", precision),
836        // MySQL-only microsecond composites: no ANSI INTERVAL-type spelling exists (they are
837        // produced only by the MySQL `interval` vocabulary, rendered in underscore form by
838        // `render_mysql_interval_unit`), so the ANSI type render uses the descriptive `TO`
839        // form for exhaustiveness — no dialect's interval-type grammar reaches these arms.
840        Some(IntervalFields::DayToMicrosecond) => {
841            render_interval_field(f, "DAY TO MICROSECOND", precision)
842        }
843        Some(IntervalFields::HourToMicrosecond) => {
844            render_interval_field(f, "HOUR TO MICROSECOND", precision)
845        }
846        Some(IntervalFields::MinuteToMicrosecond) => {
847            render_interval_field(f, "MINUTE TO MICROSECOND", precision)
848        }
849        Some(IntervalFields::SecondToMicrosecond) => {
850            render_interval_field(f, "SECOND TO MICROSECOND", precision)
851        }
852        None => match precision {
853            Some(precision) => write!(f, "({precision})"),
854            None => Ok(()),
855        },
856    }
857}
858
859fn render_interval_field(
860    f: &mut fmt::Formatter<'_>,
861    name: &str,
862    precision: Option<u32>,
863) -> fmt::Result {
864    f.write_str(" ")?;
865    render_sized(f, name, precision)
866}
867
868/// The trailing field qualifier of an interval, as a leading-space suffix
869/// (` DAY`, ` YEAR TO MONTH`, …). Used by the synthetic-literal fallback, which has
870/// no source precision to place, so this omits any `(precision)`.
871fn interval_fields_suffix(fields: IntervalFields) -> &'static str {
872    match fields {
873        IntervalFields::Year => " YEAR",
874        IntervalFields::Month => " MONTH",
875        IntervalFields::Day => " DAY",
876        IntervalFields::Hour => " HOUR",
877        IntervalFields::Minute => " MINUTE",
878        IntervalFields::Second => " SECOND",
879        IntervalFields::YearToMonth => " YEAR TO MONTH",
880        IntervalFields::DayToHour => " DAY TO HOUR",
881        IntervalFields::DayToMinute => " DAY TO MINUTE",
882        IntervalFields::DayToSecond => " DAY TO SECOND",
883        IntervalFields::HourToMinute => " HOUR TO MINUTE",
884        IntervalFields::HourToSecond => " HOUR TO SECOND",
885        IntervalFields::MinuteToSecond => " MINUTE TO SECOND",
886        IntervalFields::Week => " WEEK",
887        IntervalFields::Quarter => " QUARTER",
888        IntervalFields::Decade => " DECADE",
889        IntervalFields::Century => " CENTURY",
890        IntervalFields::Millennium => " MILLENNIUM",
891        IntervalFields::Millisecond => " MILLISECOND",
892        IntervalFields::Microsecond => " MICROSECOND",
893        IntervalFields::DayToMicrosecond => " DAY TO MICROSECOND",
894        IntervalFields::HourToMicrosecond => " HOUR TO MICROSECOND",
895        IntervalFields::MinuteToMicrosecond => " MINUTE TO MICROSECOND",
896        IntervalFields::SecondToMicrosecond => " SECOND TO MICROSECOND",
897    }
898}
899
900/// The MySQL `interval` unit vocabulary — one keyword per [`IntervalFields`], in MySQL's
901/// underscore spelling (`DAY_HOUR`, `MINUTE_SECOND`, `YEAR_MONTH`), as a leading-space
902/// suffix. This is the render counterpart of the parser's MySQL interval-unit reader: the
903/// MySQL `EVERY <expr> <unit>` event schedule reuses the shared [`IntervalFields`]
904/// vocabulary but spells its composites with an underscore, never the ANSI `TO` form. The
905/// DuckDB-only extended units (`DECADE`/`CENTURY`/… and `MILLISECOND`) are unreachable here:
906/// MySQL's `interval` production admits only the units below, so the parser never yields
907/// them for a MySQL schedule.
908fn mysql_interval_unit_suffix(unit: IntervalFields) -> &'static str {
909    match unit {
910        IntervalFields::Year => " YEAR",
911        IntervalFields::Month => " MONTH",
912        IntervalFields::Day => " DAY",
913        IntervalFields::Hour => " HOUR",
914        IntervalFields::Minute => " MINUTE",
915        IntervalFields::Second => " SECOND",
916        IntervalFields::Week => " WEEK",
917        IntervalFields::Quarter => " QUARTER",
918        IntervalFields::Microsecond => " MICROSECOND",
919        IntervalFields::YearToMonth => " YEAR_MONTH",
920        IntervalFields::DayToHour => " DAY_HOUR",
921        IntervalFields::DayToMinute => " DAY_MINUTE",
922        IntervalFields::DayToSecond => " DAY_SECOND",
923        IntervalFields::HourToMinute => " HOUR_MINUTE",
924        IntervalFields::HourToSecond => " HOUR_SECOND",
925        IntervalFields::MinuteToSecond => " MINUTE_SECOND",
926        IntervalFields::DayToMicrosecond => " DAY_MICROSECOND",
927        IntervalFields::HourToMicrosecond => " HOUR_MICROSECOND",
928        IntervalFields::MinuteToMicrosecond => " MINUTE_MICROSECOND",
929        IntervalFields::SecondToMicrosecond => " SECOND_MICROSECOND",
930        // No MySQL `interval` keyword; the parser never yields these for a MySQL schedule.
931        IntervalFields::Decade
932        | IntervalFields::Century
933        | IntervalFields::Millennium
934        | IntervalFields::Millisecond => " MICROSECOND",
935    }
936}
937
938fn render_sized(f: &mut fmt::Formatter<'_>, name: &str, size: Option<u32>) -> fmt::Result {
939    match size {
940        Some(size) => write!(f, "{name}({size})"),
941        None => f.write_str(name),
942    }
943}
944
945fn render_precision_scale(
946    f: &mut fmt::Formatter<'_>,
947    name: &str,
948    precision: Option<i32>,
949    scale: Option<i32>,
950) -> fmt::Result {
951    f.write_str(name)?;
952    match (precision, scale) {
953        (Some(precision), Some(scale)) => write!(f, "({precision}, {scale})"),
954        (Some(precision), None) => write!(f, "({precision})"),
955        (None, Some(scale)) => write!(f, "(*, {scale})"),
956        (None, None) => Ok(()),
957    }
958}
959
960/// Render a user-defined type's constant modifier list — `(3)`, `(10, 2)`, or the DuckDB
961/// string form `('OGC:CRS84')` — each modifier by its exact source spelling. Empty list
962/// renders nothing.
963///
964/// A type modifier is part of the *type*, not a value literal (`DECIMAL(10, 2)` and
965/// `DECIMAL(5, 3)` are different types), so it is rendered verbatim even in
966/// [`RenderMode::Redacted`] — matching the built-in numeric-modifier path
967/// ([`render_numeric_modifiers`]) and never masking to `?`. The exact span text
968/// round-trips the integer/string spelling; a detached node with no backing source falls
969/// back to the literal's kind-based spelling for totality.
970fn render_literal_modifiers(
971    ctx: &RenderCtx<'_>,
972    f: &mut fmt::Formatter<'_>,
973    modifiers: &[Literal],
974) -> fmt::Result {
975    if modifiers.is_empty() {
976        return Ok(());
977    }
978    f.write_str("(")?;
979    for (index, modifier) in modifiers.iter().enumerate() {
980        if index > 0 {
981            f.write_str(", ")?;
982        }
983        if let Some(text) = ctx.slice(modifier.meta.span) {
984            f.write_str(text)?;
985        } else {
986            modifier.render(ctx, f)?;
987        }
988    }
989    f.write_str(")")
990}
991
992fn render_numeric_modifiers(f: &mut fmt::Formatter<'_>, modifiers: &[u32]) -> fmt::Result {
993    if modifiers.is_empty() {
994        return Ok(());
995    }
996    f.write_str("(")?;
997    for (index, modifier) in modifiers.iter().enumerate() {
998        if index > 0 {
999            f.write_str(", ")?;
1000        }
1001        write!(f, "{modifier}")?;
1002    }
1003    f.write_str(")")
1004}
1005
1006/// Render a MySQL `ENUM(...)` / `SET(...)` value-list type. Each member renders as
1007/// its source-spelled string constant.
1008fn render_value_list_type(
1009    ctx: &RenderCtx<'_>,
1010    name: &str,
1011    values: &[Literal],
1012    f: &mut fmt::Formatter<'_>,
1013) -> fmt::Result {
1014    f.write_str(name)?;
1015    f.write_str("(")?;
1016    render_comma_separated(values, ctx, f)?;
1017    f.write_str(")")
1018}
1019
1020/// The keyword an anonymous composite type ([`DataType::Struct`]) renders under.
1021/// The ClickHouse keyword a [`DataType::Wrapped`] type combinator renders under. The
1022/// canonical mixed-case spelling round-trips ClickHouse's case-sensitive type name
1023/// (`Nullable`, never `NULLABLE`), even though the case-insensitive keyword parser
1024/// accepts any casing on input.
1025fn wrapped_type_keyword(kind: WrappedTypeKind) -> &'static str {
1026    match kind {
1027        WrappedTypeKind::Nullable => "Nullable",
1028        WrappedTypeKind::LowCardinality => "LowCardinality",
1029    }
1030}
1031
1032/// The canonical spelling of a ClickHouse [`DataType::FixedWidthInt`] type name, e.g.
1033/// `(true, IntWidth::W256)` → `Int256`, `(false, IntWidth::W8)` → `UInt8`. Mixed-case
1034/// round-trips ClickHouse's case-sensitive type name (`Int256`, never `INT256`), even
1035/// though the case-insensitive keyword parser accepts any casing on input.
1036fn fixed_width_int_name(signed: bool, width: IntWidth) -> &'static str {
1037    match (signed, width) {
1038        (true, IntWidth::W8) => "Int8",
1039        (true, IntWidth::W16) => "Int16",
1040        (true, IntWidth::W32) => "Int32",
1041        (true, IntWidth::W64) => "Int64",
1042        (true, IntWidth::W128) => "Int128",
1043        (true, IntWidth::W256) => "Int256",
1044        (false, IntWidth::W8) => "UInt8",
1045        (false, IntWidth::W16) => "UInt16",
1046        (false, IntWidth::W32) => "UInt32",
1047        (false, IntWidth::W64) => "UInt64",
1048        (false, IntWidth::W128) => "UInt128",
1049        (false, IntWidth::W256) => "UInt256",
1050    }
1051}
1052
1053fn struct_type_keyword(spelling: StructTypeSpelling) -> &'static str {
1054    match spelling {
1055        StructTypeSpelling::Struct | StructTypeSpelling::AngleBracket => "STRUCT",
1056        StructTypeSpelling::Row => "ROW",
1057    }
1058}
1059
1060/// BigQuery `STRUCT<field TYPE, …>` / angle-bracket composite list.
1061fn render_composite_type_angle<X: Extension + Render>(
1062    ctx: &RenderCtx<'_>,
1063    keyword: &str,
1064    fields: &[StructTypeField<X>],
1065    f: &mut fmt::Formatter<'_>,
1066) -> fmt::Result {
1067    f.write_str(keyword)?;
1068    f.write_str("<")?;
1069    render_comma_separated(fields, ctx, f)?;
1070    f.write_str(">")
1071}
1072
1073/// Render an anonymous composite type: `<keyword>(name TYPE, ...)`, shared by
1074/// [`DataType::Struct`] (`STRUCT`/`ROW`), [`DataType::Union`], and the ClickHouse
1075/// [`DataType::Nested`].
1076fn render_composite_type<X: Extension + Render>(
1077    ctx: &RenderCtx<'_>,
1078    keyword: &str,
1079    fields: &[StructTypeField<X>],
1080    f: &mut fmt::Formatter<'_>,
1081) -> fmt::Result {
1082    f.write_str(keyword)?;
1083    f.write_str("(")?;
1084    render_comma_separated(fields, ctx, f)?;
1085    f.write_str(")")
1086}
1087
1088/// Render an array-type suffix per its written surface: bracket `T[]`/`T[n]` or keyword
1089/// `T ARRAY`/`T ARRAY[n]`, with the fixed-size bound when present.
1090fn render_array_type<X: Extension + Render>(
1091    ctx: &RenderCtx<'_>,
1092    element: &DataType<X>,
1093    size: Option<u32>,
1094    spelling: ArrayTypeSpelling,
1095    f: &mut fmt::Formatter<'_>,
1096) -> fmt::Result {
1097    if matches!(spelling, ArrayTypeSpelling::AngleBracket) {
1098        f.write_str("ARRAY<")?;
1099        element.render(ctx, f)?;
1100        return f.write_str(">");
1101    }
1102    element.render(ctx, f)?;
1103    match spelling {
1104        ArrayTypeSpelling::Bracket => match size {
1105            Some(n) => write!(f, "[{n}]"),
1106            None => f.write_str("[]"),
1107        },
1108        ArrayTypeSpelling::Keyword => {
1109            f.write_str(" ARRAY")?;
1110            match size {
1111                Some(n) => write!(f, "[{n}]"),
1112                None => Ok(()),
1113            }
1114        }
1115        ArrayTypeSpelling::AngleBracket => unreachable!("handled above"),
1116    }
1117}
1118
1119/// Render a MySQL numeric type with its `SIGNED`/`UNSIGNED`/`ZEROFILL` modifiers,
1120/// space-separating only the parts that are present. A `None` element is the
1121/// standalone integer cast target (`CAST(x AS UNSIGNED)`).
1122fn render_numeric_modifier_type<X: Extension + Render>(
1123    ctx: &RenderCtx<'_>,
1124    element: Option<&DataType<X>>,
1125    signedness: Signedness,
1126    zerofill: bool,
1127    f: &mut fmt::Formatter<'_>,
1128) -> fmt::Result {
1129    let mut wrote = false;
1130    if let Some(element) = element {
1131        element.render(ctx, f)?;
1132        wrote = true;
1133    }
1134    let sign = match signedness {
1135        Signedness::Unspecified => None,
1136        Signedness::Signed => Some("SIGNED"),
1137        Signedness::Unsigned => Some("UNSIGNED"),
1138    };
1139    if let Some(sign) = sign {
1140        if wrote {
1141            f.write_str(" ")?;
1142        }
1143        f.write_str(sign)?;
1144        wrote = true;
1145    }
1146    if zerofill {
1147        if wrote {
1148            f.write_str(" ")?;
1149        }
1150        f.write_str("ZEROFILL")?;
1151    }
1152    Ok(())
1153}
1154
1155impl Render for BinaryOperator {
1156    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1157        // The surrounding spaces are added by the parent expression so the token
1158        // itself stays a bare operator.
1159        f.write_str(match self {
1160            BinaryOperator::Plus => "+",
1161            BinaryOperator::Minus => "-",
1162            BinaryOperator::Multiply => "*",
1163            BinaryOperator::Divide => "/",
1164            // Each spelling tag renders its own surface form so the exact source
1165            // round-trips (`%` vs the MySQL `MOD` keyword).
1166            BinaryOperator::Modulo(ModuloSpelling::Percent) => "%",
1167            BinaryOperator::Modulo(ModuloSpelling::Mod) => "MOD",
1168            // One integer-division operator; the spelling tag restores the exact source
1169            // form. Load-bearing, not cosmetic: DuckDB has no `DIV` keyword and MySQL no
1170            // `//` operator, so a normalized spelling would not re-parse (like the XOR tags).
1171            BinaryOperator::IntegerDivide(IntegerDivideSpelling::Div) => "DIV",
1172            BinaryOperator::IntegerDivide(IntegerDivideSpelling::SlashSlash) => "//",
1173            // PostgreSQL exponentiation. Distinct from the MySQL `^` bitwise-XOR spelling
1174            // above (`BitwiseXor(Caret)`): same glyph, different operator and precedence.
1175            BinaryOperator::Exponent => "^",
1176            BinaryOperator::StringConcat => "||",
1177            BinaryOperator::Contains => "@>",
1178            BinaryOperator::ContainedBy => "<@",
1179            BinaryOperator::Overlap => "&&",
1180            BinaryOperator::JsonGet => "->",
1181            BinaryOperator::JsonGetText => "->>",
1182            BinaryOperator::JsonExists => "?",
1183            BinaryOperator::JsonExistsAny => "?|",
1184            BinaryOperator::JsonExistsAll => "?&",
1185            BinaryOperator::JsonPathExists => "@?",
1186            BinaryOperator::JsonPathMatch => "@@",
1187            BinaryOperator::JsonExtractPath => "#>",
1188            BinaryOperator::JsonExtractPathText => "#>>",
1189            BinaryOperator::JsonDeletePath => "#-",
1190            BinaryOperator::BitwiseOr => "|",
1191            BinaryOperator::BitwiseAnd => "&",
1192            BinaryOperator::BitwiseShiftLeft => "<<",
1193            BinaryOperator::BitwiseShiftRight => ">>",
1194            // The two XOR spellings restore the exact source form; this is load-bearing,
1195            // not cosmetic (PostgreSQL rejects `^` as XOR, MySQL treats `#` as a comment).
1196            BinaryOperator::BitwiseXor(BitwiseXorSpelling::Hash) => "#",
1197            BinaryOperator::BitwiseXor(BitwiseXorSpelling::Caret) => "^",
1198            // One equality operator; the spelling tag restores the exact source form
1199            // (`=` vs the SQLite `==`), mirroring the modulo/regex tags above.
1200            BinaryOperator::Eq(EqualsSpelling::Single) => "=",
1201            BinaryOperator::Eq(EqualsSpelling::Double) => "==",
1202            // One inequality operator; the spelling tag restores the exact source form
1203            // (the SQL-standard `<>` vs the C-style `!=`). Both spellings parse under
1204            // every dialect, so a target re-spell and the redacted fingerprint normalize
1205            // to the canonical `<>` (unlike the `==`/`DIV`/XOR tags, this is not
1206            // load-bearing for validity), keeping the fingerprint stable.
1207            BinaryOperator::NotEq(NotEqSpelling::Bang) if honours_source_spelling(ctx) => "!=",
1208            BinaryOperator::NotEq(_) => "<>",
1209            BinaryOperator::Lt => "<",
1210            BinaryOperator::LtEq => "<=",
1211            BinaryOperator::Gt => ">",
1212            BinaryOperator::GtEq => ">=",
1213            // One null-safe-inequality operator; the spelling tag restores the exact
1214            // source form (the `IS DISTINCT FROM` keyword vs SQLite's bare `IS NOT`).
1215            BinaryOperator::IsDistinctFrom(IsDistinctFromSpelling::Keyword) => "IS DISTINCT FROM",
1216            BinaryOperator::IsDistinctFrom(IsDistinctFromSpelling::Is) => "IS NOT",
1217            // One null-safe-equality operator; the spelling tag restores the exact source
1218            // form. The keyword/`<=>` split is load-bearing, not cosmetic: MySQL rejects the
1219            // keyword form and the other dialects reject `<=>`, so a normalized render would
1220            // not re-parse. SQLite's bare `IS` folds on here too and renders back as `IS`.
1221            BinaryOperator::IsNotDistinctFrom(IsNotDistinctFromSpelling::Keyword) => {
1222                "IS NOT DISTINCT FROM"
1223            }
1224            BinaryOperator::IsNotDistinctFrom(IsNotDistinctFromSpelling::NullSafeEq) => "<=>",
1225            BinaryOperator::IsNotDistinctFrom(IsNotDistinctFromSpelling::Is) => "IS",
1226            BinaryOperator::Regexp(RegexpSpelling::Rlike) => "RLIKE",
1227            BinaryOperator::Regexp(RegexpSpelling::Regexp) => "REGEXP",
1228            BinaryOperator::Glob => "GLOB",
1229            BinaryOperator::StartsWith => "^@",
1230            BinaryOperator::Match => "MATCH",
1231            BinaryOperator::Overlaps => "OVERLAPS",
1232            BinaryOperator::And => "AND",
1233            BinaryOperator::Xor => "XOR",
1234            BinaryOperator::Or => "OR",
1235        })
1236    }
1237}
1238
1239impl Render for UnaryOperator {
1240    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1241        f.write_str(match self {
1242            UnaryOperator::Not => "NOT",
1243            UnaryOperator::Minus => "-",
1244            UnaryOperator::Plus => "+",
1245            UnaryOperator::BitwiseNot => "~",
1246            UnaryOperator::Prior => "PRIOR",
1247        })
1248    }
1249}
1250
1251impl Render for SetOperator {
1252    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1253        f.write_str(match self {
1254            SetOperator::Union => "UNION",
1255            SetOperator::Intersect => "INTERSECT",
1256            SetOperator::Except => "EXCEPT",
1257        })
1258    }
1259}
1260
1261impl Render for Quantifier {
1262    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1263        f.write_str(match self {
1264            Quantifier::Any => "ANY",
1265            Quantifier::All => "ALL",
1266            Quantifier::Some => "SOME",
1267        })
1268    }
1269}
1270
1271impl Render for SetQuantifier {
1272    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1273        f.write_str(match self {
1274            SetQuantifier::All => "ALL",
1275            SetQuantifier::Distinct => "DISTINCT",
1276        })
1277    }
1278}
1279
1280// ---------------------------------------------------------------------------
1281// Expressions and bp-derived parenthesization
1282// ---------------------------------------------------------------------------
1283
1284impl<X: Extension + Render> Render for Expr<X> {
1285    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1286        match self {
1287            Expr::Column { name, .. } => name.render(ctx, f),
1288            Expr::Literal { literal, .. } => literal.render(ctx, f),
1289            Expr::BinaryOp {
1290                left, op, right, ..
1291            } => {
1292                let full = ctx.mode() == RenderMode::Parenthesized;
1293                open_group(full, f)?;
1294                render_operand(
1295                    left,
1296                    binary_child_needs_parens(&ctx.target().binding_powers, op, left, Side::Left),
1297                    ctx,
1298                    f,
1299                )?;
1300                f.write_str(" ")?;
1301                op.render(ctx, f)?;
1302                f.write_str(" ")?;
1303                render_operand(
1304                    right,
1305                    binary_child_needs_parens(&ctx.target().binding_powers, op, right, Side::Right),
1306                    ctx,
1307                    f,
1308                )?;
1309                close_group(full, f)
1310            }
1311            Expr::UnaryOp { op, expr, .. } => {
1312                let full = ctx.mode() == RenderMode::Parenthesized;
1313                open_group(full, f)?;
1314                op.render(ctx, f)?;
1315                // `NOT` and `PRIOR` are alphabetic and must be separated from their
1316                // operand; the sign operators bind tight against theirs (`-a`, `+a`).
1317                if matches!(op, UnaryOperator::Not | UnaryOperator::Prior) {
1318                    f.write_str(" ")?;
1319                }
1320                render_operand(
1321                    expr,
1322                    prefix_operand_needs_parens(&ctx.target().binding_powers, op, expr),
1323                    ctx,
1324                    f,
1325                )?;
1326                close_group(full, f)
1327            }
1328            Expr::Function { call, .. } => call.render(ctx, f),
1329            Expr::Case { case, .. } => case.render(ctx, f),
1330            Expr::Extract { extract, .. } => extract.render(ctx, f),
1331            Expr::JsonFunc { json_func, .. } => json_func.render(ctx, f),
1332            Expr::JsonObject { json_object, .. } => json_object.render(ctx, f),
1333            Expr::JsonArray { json_array, .. } => json_array.render(ctx, f),
1334            Expr::JsonAggregate { json_aggregate, .. } => json_aggregate.render(ctx, f),
1335            Expr::JsonConstructor {
1336                json_constructor, ..
1337            } => json_constructor.render(ctx, f),
1338            Expr::IsJson { is_json, .. } => is_json.render(ctx, f),
1339            Expr::XmlFunc { xml_func, .. } => xml_func.render(ctx, f),
1340            Expr::StringFunc { string_func, .. } => string_func.render(ctx, f),
1341            Expr::IsDocument { expr, negated, .. } => {
1342                let full = ctx.mode() == RenderMode::Parenthesized;
1343                open_group(full, f)?;
1344                render_predicate_operand(
1345                    expr,
1346                    ctx.target().binding_powers.predicate(),
1347                    Side::Left,
1348                    ctx,
1349                    f,
1350                )?;
1351                f.write_str(if *negated {
1352                    " IS NOT DOCUMENT"
1353                } else {
1354                    " IS DOCUMENT"
1355                })?;
1356                close_group(full, f)
1357            }
1358            Expr::Cast {
1359                expr,
1360                data_type,
1361                syntax,
1362                try_cast,
1363                ..
1364            } => match syntax {
1365                // The `try` flag only reaches the `Call` spelling — DuckDB's `TRY_CAST`
1366                // has no `::`/prefix form — so it selects the `TRY_CAST(` vs `CAST(` lead.
1367                CastSyntax::Call => {
1368                    f.write_str(if *try_cast { "TRY_CAST(" } else { "CAST(" })?;
1369                    expr.render(ctx, f)?;
1370                    f.write_str(" AS ")?;
1371                    data_type.render(ctx, f)?;
1372                    f.write_str(")")
1373                }
1374                CastSyntax::DoubleColon => {
1375                    let full = ctx.mode() == RenderMode::Parenthesized;
1376                    open_group(full, f)?;
1377                    render_pg_operand(
1378                        expr,
1379                        ctx.target().binding_powers.typecast,
1380                        Side::Left,
1381                        ctx,
1382                        f,
1383                    )?;
1384                    f.write_str("::")?;
1385                    data_type.render(ctx, f)?;
1386                    close_group(full, f)
1387                }
1388                // `type 'string'`: the type name ahead of its string constant. It is a
1389                // primary (atom-like, like a `CAST(...)` call), so it never self-wraps
1390                // for the `Parenthesized` oracle mode and its string operand — always a
1391                // literal — renders verbatim without operand parentheses.
1392                CastSyntax::PrefixTyped => {
1393                    data_type.render(ctx, f)?;
1394                    f.write_str(" ")?;
1395                    expr.render(ctx, f)
1396                }
1397                // MySQL comma-form cast — a primary (atom-like) `CONVERT(<expr>, <type>)`
1398                // call, like the `CAST(...)` spelling above.
1399                CastSyntax::Convert => {
1400                    f.write_str("CONVERT(")?;
1401                    expr.render(ctx, f)?;
1402                    f.write_str(", ")?;
1403                    data_type.render(ctx, f)?;
1404                    f.write_str(")")
1405                }
1406            },
1407            Expr::IsNull {
1408                expr,
1409                negated,
1410                spelling,
1411                ..
1412            } => {
1413                let full = ctx.mode() == RenderMode::Parenthesized;
1414                open_group(full, f)?;
1415                render_predicate_operand(
1416                    expr,
1417                    ctx.target().binding_powers.predicate(),
1418                    Side::Left,
1419                    ctx,
1420                    f,
1421                )?;
1422                f.write_str(match (spelling, negated) {
1423                    (NullTestSpelling::Is, false) => " IS NULL",
1424                    (NullTestSpelling::Is, true) => " IS NOT NULL",
1425                    (NullTestSpelling::Postfix, false) => " ISNULL",
1426                    (NullTestSpelling::Postfix, true) => " NOTNULL",
1427                    // The two-word postfix is only ever produced with `negated: true`.
1428                    (NullTestSpelling::PostfixNotNull, _) => " NOT NULL",
1429                })?;
1430                close_group(full, f)
1431            }
1432            Expr::IsTruth {
1433                expr,
1434                value,
1435                negated,
1436                ..
1437            } => {
1438                let full = ctx.mode() == RenderMode::Parenthesized;
1439                open_group(full, f)?;
1440                render_predicate_operand(
1441                    expr,
1442                    ctx.target().binding_powers.predicate(),
1443                    Side::Left,
1444                    ctx,
1445                    f,
1446                )?;
1447                f.write_str(match (negated, value) {
1448                    (false, TruthValue::True) => " IS TRUE",
1449                    (true, TruthValue::True) => " IS NOT TRUE",
1450                    (false, TruthValue::False) => " IS FALSE",
1451                    (true, TruthValue::False) => " IS NOT FALSE",
1452                    (false, TruthValue::Unknown) => " IS UNKNOWN",
1453                    (true, TruthValue::Unknown) => " IS NOT UNKNOWN",
1454                })?;
1455                close_group(full, f)
1456            }
1457            Expr::IsNormalized {
1458                expr,
1459                form,
1460                negated,
1461                ..
1462            } => {
1463                let full = ctx.mode() == RenderMode::Parenthesized;
1464                open_group(full, f)?;
1465                render_predicate_operand(
1466                    expr,
1467                    ctx.target().binding_powers.predicate(),
1468                    Side::Left,
1469                    ctx,
1470                    f,
1471                )?;
1472                f.write_str(if *negated { " IS NOT " } else { " IS " })?;
1473                if let Some(form) = form {
1474                    f.write_str(match form {
1475                        NormalizationForm::Nfc => "NFC ",
1476                        NormalizationForm::Nfd => "NFD ",
1477                        NormalizationForm::Nfkc => "NFKC ",
1478                        NormalizationForm::Nfkd => "NFKD ",
1479                    })?;
1480                }
1481                f.write_str("NORMALIZED")?;
1482                close_group(full, f)
1483            }
1484            Expr::Between {
1485                expr,
1486                low,
1487                high,
1488                negated,
1489                symmetric,
1490                ..
1491            } => {
1492                let full = ctx.mode() == RenderMode::Parenthesized;
1493                open_group(full, f)?;
1494                let range = ctx.target().binding_powers.range_predicate();
1495                render_predicate_operand(expr, range, Side::Left, ctx, f)?;
1496                f.write_str(if *negated {
1497                    " NOT BETWEEN "
1498                } else {
1499                    " BETWEEN "
1500                })?;
1501                if *symmetric {
1502                    f.write_str("SYMMETRIC ")?;
1503                }
1504                render_predicate_operand(low, range, Side::Right, ctx, f)?;
1505                f.write_str(" AND ")?;
1506                render_predicate_operand(high, range, Side::Right, ctx, f)?;
1507                close_group(full, f)
1508            }
1509            Expr::Like {
1510                expr,
1511                pattern,
1512                escape,
1513                negated,
1514                spelling,
1515                ..
1516            } => {
1517                let full = ctx.mode() == RenderMode::Parenthesized;
1518                open_group(full, f)?;
1519                let range = ctx.target().binding_powers.range_predicate();
1520                render_predicate_operand(expr, range, Side::Left, ctx, f)?;
1521                f.write_str(match (negated, spelling) {
1522                    (false, LikeSpelling::Like) => " LIKE ",
1523                    (true, LikeSpelling::Like) => " NOT LIKE ",
1524                    (false, LikeSpelling::ILike) => " ILIKE ",
1525                    (true, LikeSpelling::ILike) => " NOT ILIKE ",
1526                    (false, LikeSpelling::SimilarTo) => " SIMILAR TO ",
1527                    (true, LikeSpelling::SimilarTo) => " NOT SIMILAR TO ",
1528                })?;
1529                render_predicate_operand(pattern, range, Side::Right, ctx, f)?;
1530                if let Some(escape) = escape {
1531                    f.write_str(" ESCAPE ")?;
1532                    render_predicate_operand(escape, range, Side::Right, ctx, f)?;
1533                }
1534                close_group(full, f)
1535            }
1536            Expr::InList {
1537                expr,
1538                list,
1539                negated,
1540                ..
1541            } => {
1542                let full = ctx.mode() == RenderMode::Parenthesized;
1543                open_group(full, f)?;
1544                render_predicate_operand(
1545                    expr,
1546                    ctx.target().binding_powers.range_predicate(),
1547                    Side::Left,
1548                    ctx,
1549                    f,
1550                )?;
1551                f.write_str(if *negated { " NOT IN (" } else { " IN (" })?;
1552                render_comma_separated(list, ctx, f)?;
1553                f.write_str(")")?;
1554                close_group(full, f)
1555            }
1556            Expr::InSubquery {
1557                expr,
1558                subquery,
1559                negated,
1560                ..
1561            } => {
1562                let full = ctx.mode() == RenderMode::Parenthesized;
1563                open_group(full, f)?;
1564                render_predicate_operand(
1565                    expr,
1566                    ctx.target().binding_powers.range_predicate(),
1567                    Side::Left,
1568                    ctx,
1569                    f,
1570                )?;
1571                f.write_str(if *negated { " NOT IN " } else { " IN " })?;
1572                render_query_in_parens(subquery, ctx, f)?;
1573                close_group(full, f)
1574            }
1575            Expr::InExpr {
1576                expr, rhs, negated, ..
1577            } => {
1578                // DuckDB's unparenthesized `IN <value>` binds at its own rank
1579                // (`UNPARENTHESIZED_IN_LIST`), tighter than the comparison predicates, so
1580                // both operands parenthesize by the binding-power oracle against that rank
1581                // rather than the predicate level.
1582                let table = &ctx.target().binding_powers;
1583                let full = ctx.mode() == RenderMode::Parenthesized;
1584                open_group(full, f)?;
1585                render_operand(
1586                    expr,
1587                    operand_needs_parens(table, UNPARENTHESIZED_IN_LIST, expr, Side::Left),
1588                    ctx,
1589                    f,
1590                )?;
1591                f.write_str(if *negated { " NOT IN " } else { " IN " })?;
1592                render_operand(
1593                    rhs,
1594                    operand_needs_parens(table, UNPARENTHESIZED_IN_LIST, rhs, Side::Right),
1595                    ctx,
1596                    f,
1597                )?;
1598                close_group(full, f)
1599            }
1600            Expr::Exists { query, .. } => {
1601                let full = ctx.mode() == RenderMode::Parenthesized;
1602                open_group(full, f)?;
1603                f.write_str("EXISTS ")?;
1604                render_query_in_parens(query, ctx, f)?;
1605                close_group(full, f)
1606            }
1607            Expr::QuantifiedComparison {
1608                left,
1609                op,
1610                quantifier,
1611                subquery,
1612                ..
1613            } => {
1614                let full = ctx.mode() == RenderMode::Parenthesized;
1615                open_group(full, f)?;
1616                render_predicate_operand(
1617                    left,
1618                    ctx.target().binding_powers.comparison,
1619                    Side::Left,
1620                    ctx,
1621                    f,
1622                )?;
1623                f.write_str(" ")?;
1624                op.render(ctx, f)?;
1625                f.write_str(" ")?;
1626                quantifier.render(ctx, f)?;
1627                f.write_str(" ")?;
1628                render_query_in_parens(subquery, ctx, f)?;
1629                close_group(full, f)
1630            }
1631            Expr::QuantifiedList {
1632                left,
1633                op,
1634                quantifier,
1635                array,
1636                ..
1637            } => {
1638                let full = ctx.mode() == RenderMode::Parenthesized;
1639                open_group(full, f)?;
1640                render_predicate_operand(
1641                    left,
1642                    ctx.target().binding_powers.comparison,
1643                    Side::Left,
1644                    ctx,
1645                    f,
1646                )?;
1647                f.write_str(" ")?;
1648                op.render(ctx, f)?;
1649                f.write_str(" ")?;
1650                quantifier.render(ctx, f)?;
1651                f.write_str(" (")?;
1652                array.render(ctx, f)?;
1653                f.write_str(")")?;
1654                close_group(full, f)
1655            }
1656            Expr::QuantifiedLike {
1657                left,
1658                pattern,
1659                quantifier,
1660                negated,
1661                spelling,
1662                ..
1663            } => {
1664                let full = ctx.mode() == RenderMode::Parenthesized;
1665                open_group(full, f)?;
1666                render_predicate_operand(
1667                    left,
1668                    ctx.target().binding_powers.range_predicate(),
1669                    Side::Left,
1670                    ctx,
1671                    f,
1672                )?;
1673                f.write_str(match (negated, spelling) {
1674                    (false, LikeSpelling::Like) => " LIKE ",
1675                    (true, LikeSpelling::Like) => " NOT LIKE ",
1676                    (false, LikeSpelling::ILike) => " ILIKE ",
1677                    (true, LikeSpelling::ILike) => " NOT ILIKE ",
1678                    // `SIMILAR TO` has no quantified form; the parser never builds it, so
1679                    // these arms are unreachable but kept total for the match.
1680                    (false, LikeSpelling::SimilarTo) => " SIMILAR TO ",
1681                    (true, LikeSpelling::SimilarTo) => " NOT SIMILAR TO ",
1682                })?;
1683                quantifier.render(ctx, f)?;
1684                f.write_str(" (")?;
1685                pattern.render(ctx, f)?;
1686                f.write_str(")")?;
1687                close_group(full, f)
1688            }
1689            Expr::Subquery { query, .. } => render_query_in_parens(query, ctx, f),
1690            // A placeholder's identity is query structure, not a value, so it renders
1691            // verbatim in every mode.
1692            Expr::Parameter { kind, .. } => render_parameter_kind(*kind, ctx, f),
1693            // A positional column reference's identity — its 1-based index — is query
1694            // structure, not a value, so it renders verbatim in every mode (never masked
1695            // the way a `Literal` value is), like the positional parameter above.
1696            Expr::PositionalColumn { index, .. } => write!(f, "#{index}"),
1697            // A session variable's identity — its sigil, optional scope, and name — is
1698            // query structure, not a value, so it renders verbatim in every mode (the
1699            // name is never masked the way a `Literal` value is), like the named
1700            // placeholder above. The `kind` tag restores the sigil and the canonical
1701            // lowercase scope keyword so all four forms round-trip.
1702            Expr::SessionVariable { kind, name, .. } => {
1703                let prefix = match kind {
1704                    SessionVariableKind::User => "@",
1705                    SessionVariableKind::System => "@@",
1706                    SessionVariableKind::SystemGlobal => "@@global.",
1707                    SessionVariableKind::SystemSession => "@@session.",
1708                };
1709                write!(f, "{prefix}{}", ctx.resolve(*name))
1710            }
1711            Expr::Subscript { subscript, .. } => {
1712                let full = ctx.mode() == RenderMode::Parenthesized;
1713                open_group(full, f)?;
1714                // A `::`-cast base must be parenthesized even though the typecast binds
1715                // tighter than the subscript: the cast's rendered type-tail would
1716                // otherwise re-absorb the following `[` as an array-type suffix
1717                // (`a::INT[1]` is a cast to `INT[1]`, not a subscript of `a::INT`), so
1718                // binding power alone would wrongly drop the parens. The `CAST(...)` call
1719                // and prefixed `TYPE 'string'` spellings self-delimit and are unaffected.
1720                let base_needs_parens = matches!(
1721                    subscript.base,
1722                    Expr::Cast {
1723                        syntax: CastSyntax::DoubleColon,
1724                        ..
1725                    }
1726                ) || operand_needs_parens(
1727                    &ctx.target().binding_powers,
1728                    ctx.target().binding_powers.subscript,
1729                    &subscript.base,
1730                    Side::Left,
1731                );
1732                render_operand(&subscript.base, base_needs_parens, ctx, f)?;
1733                f.write_str("[")?;
1734                match subscript.kind {
1735                    SubscriptKind::Index => {
1736                        // A bare index carries its single value in `lower`.
1737                        if let Some(index) = &subscript.lower {
1738                            index.render(ctx, f)?;
1739                        }
1740                    }
1741                    SubscriptKind::Slice => {
1742                        if let Some(lower) = &subscript.lower {
1743                            lower.render(ctx, f)?;
1744                        }
1745                        f.write_str(":")?;
1746                        if let Some(upper) = &subscript.upper {
1747                            upper.render(ctx, f)?;
1748                        }
1749                    }
1750                    SubscriptKind::SliceWithStep => {
1751                        if let Some(lower) = &subscript.lower {
1752                            lower.render(ctx, f)?;
1753                        }
1754                        f.write_str(":")?;
1755                        // The middle bound is mandatory; an omitted upper is DuckDB's `-`
1756                        // open-upper placeholder, so a `None` renders as `-`, not empty.
1757                        match &subscript.upper {
1758                            Some(upper) => upper.render(ctx, f)?,
1759                            None => f.write_str("-")?,
1760                        }
1761                        f.write_str(":")?;
1762                        if let Some(step) = &subscript.step {
1763                            step.render(ctx, f)?;
1764                        }
1765                    }
1766                }
1767                f.write_str("]")?;
1768                close_group(full, f)
1769            }
1770            Expr::SemiStructuredAccess {
1771                semi_structured_access,
1772                ..
1773            } => semi_structured_access.render(ctx, f),
1774            Expr::Collate { collate, .. } => {
1775                let full = ctx.mode() == RenderMode::Parenthesized;
1776                open_group(full, f)?;
1777                render_pg_operand(
1778                    &collate.expr,
1779                    ctx.target().binding_powers.collate,
1780                    Side::Left,
1781                    ctx,
1782                    f,
1783                )?;
1784                f.write_str(" COLLATE ")?;
1785                collate.collation.render(ctx, f)?;
1786                close_group(full, f)
1787            }
1788            Expr::AtTimeZone { at_time_zone, .. } => {
1789                let full = ctx.mode() == RenderMode::Parenthesized;
1790                open_group(full, f)?;
1791                let bp = ctx.target().binding_powers.at_time_zone;
1792                render_pg_operand(&at_time_zone.expr, bp, Side::Left, ctx, f)?;
1793                f.write_str(" AT TIME ZONE ")?;
1794                // The zone is the right operand, parsed at the operator's right
1795                // binding power, so it parenthesizes by the same rule.
1796                render_pg_operand(&at_time_zone.zone, bp, Side::Right, ctx, f)?;
1797                close_group(full, f)
1798            }
1799            Expr::Interval { value, unit, .. } => {
1800                // The MySQL operator-position interval quantity, a primary (highest binding
1801                // power): the amount is terminated by the unit keyword, so it renders as a bare
1802                // sub-expression with no operator-precedence parens. The unit reuses the shared
1803                // IntervalFields vocabulary in MySQL underscore spelling (never the ANSI `TO`
1804                // form); its suffix carries its own leading space.
1805                let full = ctx.mode() == RenderMode::Parenthesized;
1806                open_group(full, f)?;
1807                f.write_str("INTERVAL ")?;
1808                value.render(ctx, f)?;
1809                f.write_str(mysql_interval_unit_suffix(*unit))?;
1810                close_group(full, f)
1811            }
1812            Expr::Array { array, .. } => match &**array {
1813                ArrayExpr::Elements {
1814                    elements, spelling, ..
1815                } => {
1816                    f.write_str(match spelling {
1817                        ArraySpelling::Keyword => "ARRAY[",
1818                        ArraySpelling::Bracket => "[",
1819                    })?;
1820                    render_comma_separated(elements, ctx, f)?;
1821                    f.write_str("]")
1822                }
1823                ArrayExpr::Subquery { query, .. } => {
1824                    f.write_str("ARRAY")?;
1825                    render_query_in_parens(query, ctx, f)
1826                }
1827                ArrayExpr::Comprehension { comprehension, .. } => {
1828                    f.write_str("[")?;
1829                    comprehension.element.render(ctx, f)?;
1830                    f.write_str(" for ")?;
1831                    render_ident_list(&comprehension.vars, ctx, f)?;
1832                    f.write_str(" in ")?;
1833                    render_comprehension_source(&comprehension.source, ctx, f)?;
1834                    if let Some(filter) = &comprehension.filter {
1835                        f.write_str(" if ")?;
1836                        filter.render(ctx, f)?;
1837                    }
1838                    f.write_str("]")
1839                }
1840            },
1841            Expr::Struct { r#struct, .. } => {
1842                f.write_str("{")?;
1843                for (i, field) in r#struct.fields.iter().enumerate() {
1844                    if i > 0 {
1845                        f.write_str(", ")?;
1846                    }
1847                    field.render(ctx, f)?;
1848                }
1849                f.write_str("}")
1850            }
1851            Expr::StructConstructor { constructor, .. } => {
1852                f.write_str("STRUCT")?;
1853                if !constructor.fields.is_empty() {
1854                    f.write_str("<")?;
1855                    render_comma_separated(&constructor.fields, ctx, f)?;
1856                    f.write_str(">")?;
1857                }
1858                f.write_str("(")?;
1859                render_comma_separated(&constructor.args, ctx, f)?;
1860                f.write_str(")")
1861            }
1862            Expr::Map { map, .. } => {
1863                f.write_str("MAP {")?;
1864                for (i, entry) in map.entries.iter().enumerate() {
1865                    if i > 0 {
1866                        f.write_str(", ")?;
1867                    }
1868                    entry.key.render(ctx, f)?;
1869                    f.write_str(": ")?;
1870                    entry.value.render(ctx, f)?;
1871                }
1872                f.write_str("}")
1873            }
1874            Expr::Row { row, .. } => {
1875                f.write_str(if row.explicit { "ROW(" } else { "(" })?;
1876                render_comma_separated(&row.fields, ctx, f)?;
1877                f.write_str(")")
1878            }
1879            Expr::FieldSelection {
1880                field_selection, ..
1881            } => {
1882                // The base is always parenthesized so `(c).f` cannot re-parse as the
1883                // qualified column `c.f`; the `.*` star form keeps the same parens so a
1884                // whole-row `tbl.*` value renders `(tbl).*` (re-parses to the same node).
1885                f.write_str("(")?;
1886                field_selection.base.render(ctx, f)?;
1887                f.write_str(").")?;
1888                match &field_selection.selector {
1889                    FieldSelector::Field { field, .. } => field.render(ctx, f),
1890                    FieldSelector::Star { .. } => f.write_str("*"),
1891                }
1892            }
1893            Expr::SpecialFunction {
1894                keyword, precision, ..
1895            } => {
1896                f.write_str(special_function_keyword(*keyword))?;
1897                if let Some(precision) = precision {
1898                    write!(f, "({precision})")?;
1899                }
1900                Ok(())
1901            }
1902            // A PostgreSQL infix operator at the "any other operator" rank (the
1903            // `any_operator` level, ADR-0008), so it routes through the same
1904            // binding-power-driven operand parenthesization as the extension infix
1905            // operators. The bare spelling renders `a op b` (`a ~ b`, `a <-> b`); the
1906            // explicit form renders `a OPERATOR(schema.op) b`, carrying its optional schema
1907            // qualification. PostgreSQL's own deparse keeps the two apart (a bare `~` stays
1908            // bare, `OPERATOR(pg_catalog.+)` stays wrapped), so the spelling tag is
1909            // load-bearing for round-trip fidelity.
1910            Expr::NamedOperator { named_operator, .. } => render_extension_infix(
1911                ctx,
1912                f,
1913                ctx.target().binding_powers.any_operator,
1914                (&named_operator.left, &named_operator.right),
1915                |f| match named_operator.spelling {
1916                    NamedOperatorSpelling::Bare => {
1917                        f.write_str(" ")?;
1918                        f.write_str(ctx.resolve(named_operator.op))?;
1919                        f.write_str(" ")
1920                    }
1921                    NamedOperatorSpelling::OperatorKeyword => {
1922                        f.write_str(" OPERATOR(")?;
1923                        for part in &named_operator.schema.0 {
1924                            part.render(ctx, f)?;
1925                            f.write_str(".")?;
1926                        }
1927                        f.write_str(ctx.resolve(named_operator.op))?;
1928                        f.write_str(") ")
1929                    }
1930                },
1931            ),
1932            // A PostgreSQL prefix operator (`@ x`, `|/ x`, `@@ box`, `@#@ x`) at the "any
1933            // other operator" rank. The trailing space keeps the operator a bare token and
1934            // stops the operand's own lead byte (`-5`, another operator) from re-lexing into
1935            // a longer operator.
1936            Expr::PrefixOperator {
1937                prefix_operator, ..
1938            } => render_extension_prefix(
1939                ctx,
1940                f,
1941                ctx.target().binding_powers.any_operator.left,
1942                |f| {
1943                    f.write_str(ctx.resolve(prefix_operator.op))?;
1944                    f.write_str(" ")
1945                },
1946                &prefix_operator.operand,
1947            ),
1948            // A DuckDB postfix operator (`10 !`, `1 ~`, `1 <->`) at the "any other operator"
1949            // rank. The leading space keeps the operator a bare token so the operand's own
1950            // trailing byte does not re-lex into a longer operator.
1951            Expr::PostfixOperator {
1952                postfix_operator, ..
1953            } => render_extension_postfix(
1954                ctx,
1955                f,
1956                ctx.target().binding_powers.any_operator,
1957                &postfix_operator.operand,
1958                |f| {
1959                    f.write_str(" ")?;
1960                    f.write_str(ctx.resolve(postfix_operator.op))
1961                },
1962            ),
1963            // The DuckDB lambda `->` is the JSON-arrow token at the JSON-arrow rank,
1964            // so its body parenthesizes as that operator's right operand. The
1965            // parameter side is closed (bare idents), never parenthesized beyond its
1966            // recorded spelling.
1967            Expr::Lambda { lambda, .. } => {
1968                let full = ctx.mode() == RenderMode::Parenthesized;
1969                open_group(full, f)?;
1970                match lambda.spelling {
1971                    // The python-style spelling `lambda <params>: <body>`: the `:` and the
1972                    // enclosing delimiter bound the body, so it renders like a function
1973                    // argument — no operator-precedence parens, unlike the arrow forms.
1974                    LambdaParamSpelling::Keyword => {
1975                        f.write_str("lambda ")?;
1976                        render_comma_separated(&lambda.params, ctx, f)?;
1977                        f.write_str(": ")?;
1978                        lambda.body.render(ctx, f)?;
1979                    }
1980                    arrow => {
1981                        match arrow {
1982                            // `Bare` implies one parameter (a parser invariant); rendering
1983                            // falls back to the parenthesized list on a synthesized
1984                            // multi-parameter value so output stays re-parseable.
1985                            LambdaParamSpelling::Bare if lambda.params.len() == 1 => {
1986                                lambda.params[0].render(ctx, f)?;
1987                            }
1988                            LambdaParamSpelling::RowKeyword => {
1989                                f.write_str("ROW(")?;
1990                                render_comma_separated(&lambda.params, ctx, f)?;
1991                                f.write_str(")")?;
1992                            }
1993                            // `Parenthesized`, or a synthesized multi-parameter `Bare`.
1994                            _ => {
1995                                f.write_str("(")?;
1996                                render_comma_separated(&lambda.params, ctx, f)?;
1997                                f.write_str(")")?;
1998                            }
1999                        }
2000                        f.write_str(" -> ")?;
2001                        render_pg_operand(
2002                            &lambda.body,
2003                            ctx.target().binding_powers.binary(&BinaryOperator::JsonGet),
2004                            Side::Right,
2005                            ctx,
2006                            f,
2007                        )?;
2008                    }
2009                }
2010                close_group(full, f)
2011            }
2012            // The DuckDB star node in its three spellings (`spelling`): the wrapped
2013            // `COLUMNS(<pattern>)` / star `COLUMNS(*)` / `COLUMNS(t.*)`, the `*COLUMNS(…)`
2014            // unpack prefix, and the bare `*` / `t.*` written without the wrapper — each
2015            // carrying the wildcard modifiers on its star form (`COLUMNS(* EXCLUDE (i))`,
2016            // `* EXCLUDE (id)`). An atom like a call: no self-parenthesization, the parent
2017            // decides grouping.
2018            Expr::Columns {
2019                qualifier,
2020                pattern,
2021                options,
2022                spelling,
2023                ..
2024            } => {
2025                if matches!(spelling, ColumnsSpelling::Star) {
2026                    // The bare star has no `COLUMNS(...)` wrapper and never a pattern.
2027                    if let Some(qualifier) = qualifier {
2028                        qualifier.render(ctx, f)?;
2029                        f.write_str(".")?;
2030                    }
2031                    f.write_str("*")?;
2032                    if let Some(options) = options {
2033                        render_wildcard_options(options, ctx, f)?;
2034                    }
2035                    return Ok(());
2036                }
2037                if matches!(spelling, ColumnsSpelling::Unpack) {
2038                    f.write_str("*")?;
2039                }
2040                f.write_str("COLUMNS(")?;
2041                match pattern {
2042                    Some(pattern) => pattern.render(ctx, f)?,
2043                    None => {
2044                        if let Some(qualifier) = qualifier {
2045                            qualifier.render(ctx, f)?;
2046                            f.write_str(".")?;
2047                        }
2048                        f.write_str("*")?;
2049                        if let Some(options) = options {
2050                            render_wildcard_options(options, ctx, f)?;
2051                        }
2052                    }
2053                }
2054                f.write_str(")")
2055            }
2056            Expr::Other { ext, .. } => ext.render(ctx, f),
2057        }
2058    }
2059}
2060
2061/// The canonical uppercase spelling of a SQL special value function keyword.
2062fn special_function_keyword(keyword: SpecialFunctionKeyword) -> &'static str {
2063    match keyword {
2064        SpecialFunctionKeyword::CurrentCatalog => "CURRENT_CATALOG",
2065        SpecialFunctionKeyword::CurrentDate => "CURRENT_DATE",
2066        SpecialFunctionKeyword::CurrentRole => "CURRENT_ROLE",
2067        SpecialFunctionKeyword::CurrentSchema => "CURRENT_SCHEMA",
2068        SpecialFunctionKeyword::CurrentTime => "CURRENT_TIME",
2069        SpecialFunctionKeyword::CurrentTimestamp => "CURRENT_TIMESTAMP",
2070        SpecialFunctionKeyword::CurrentUser => "CURRENT_USER",
2071        SpecialFunctionKeyword::LocalTime => "LOCALTIME",
2072        SpecialFunctionKeyword::LocalTimestamp => "LOCALTIMESTAMP",
2073        SpecialFunctionKeyword::SessionUser => "SESSION_USER",
2074        SpecialFunctionKeyword::SystemUser => "SYSTEM_USER",
2075        SpecialFunctionKeyword::User => "USER",
2076        SpecialFunctionKeyword::UtcDate => "UTC_DATE",
2077        SpecialFunctionKeyword::UtcTime => "UTC_TIME",
2078        SpecialFunctionKeyword::UtcTimestamp => "UTC_TIMESTAMP",
2079    }
2080}
2081
2082fn open_group(group: bool, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2083    if group {
2084        f.write_str("(")?;
2085    }
2086    Ok(())
2087}
2088
2089fn close_group(group: bool, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2090    if group {
2091        f.write_str(")")?;
2092    }
2093    Ok(())
2094}
2095
2096/// Render a child expression, adding parentheses when required.
2097///
2098/// In `Parenthesized` mode every binary/unary node already wraps itself, so the
2099/// parent contributes nothing; in the other modes it adds exactly the parens the
2100/// binding-power table demands (`canonical_parens`).
2101fn render_operand<X: Extension + Render>(
2102    child: &Expr<X>,
2103    canonical_parens: bool,
2104    ctx: &RenderCtx<'_>,
2105    f: &mut fmt::Formatter<'_>,
2106) -> fmt::Result {
2107    let wrap = match ctx.mode() {
2108        RenderMode::Parenthesized => false,
2109        RenderMode::Canonical | RenderMode::Redacted => canonical_parens,
2110    };
2111    open_group(wrap, f)?;
2112    child.render(ctx, f)?;
2113    close_group(wrap, f)
2114}
2115
2116/// Render an operand of an `IS NULL` / `BETWEEN` / `IN` predicate.
2117///
2118/// The operand is parenthesized by the same binding-power rule as any other child rather than
2119/// conservatively whenever it is compound: only when it binds looser than the predicate's own
2120/// level, or is an equal-precedence non-associative sibling. `side` is `Left` for the principal
2121/// operand (left of the keyword) and `Right` for a `BETWEEN` bound / `LIKE` pattern, which the
2122/// parser parses at the predicate's right binding power.
2123///
2124/// `parent` is the binding power the predicate node itself binds at — `bp.predicate()` for the
2125/// `IS`-family (`IS NULL`/`IS TRUE`/`IS DISTINCT FROM`…), `bp.range_predicate()` for the
2126/// range/pattern/membership family (`BETWEEN`/`LIKE`/`IN`…), `bp.comparison` for a quantified
2127/// comparison — so the operand is grouped against the SAME rank the parser climbed the
2128/// predicate at (ADR-0008). Child-shape dispatch is the shared [`operand_needs_parens`] oracle,
2129/// so a predicate operand parenthesizes by exactly the same rule as any other operand position.
2130fn render_predicate_operand<X: Extension + Render>(
2131    child: &Expr<X>,
2132    parent: BindingPower,
2133    side: Side,
2134    ctx: &RenderCtx<'_>,
2135    f: &mut fmt::Formatter<'_>,
2136) -> fmt::Result {
2137    render_operand(
2138        child,
2139        operand_needs_parens(&ctx.target().binding_powers, parent, child, side),
2140        ctx,
2141        f,
2142    )
2143}
2144
2145fn render_query_in_parens<X: Extension + Render>(
2146    query: &Query<X>,
2147    ctx: &RenderCtx<'_>,
2148    f: &mut fmt::Formatter<'_>,
2149) -> fmt::Result {
2150    f.write_str("(")?;
2151    query.render(ctx, f)?;
2152    f.write_str(")")
2153}
2154
2155/// Whether a binary parent's child needs parens on the given side.
2156///
2157/// Binary children defer to the authoritative [`BindingPowerTable::needs_parens`]
2158/// oracle (which also encodes equal-precedence associativity), read from the
2159/// target dialect's table so render-time grouping honours the same per-dialect
2160/// binding powers the parser used. A prefix-operator child closes its
2161/// left edge, so it only needs parens as a *left* operand whose binding power the
2162/// parent's left side outbinds (e.g. `(NOT a) * b`). A comparison-level predicate
2163/// (`IS NULL` / `BETWEEN` / `IN`) binds at the predicate level, so it parenthesizes
2164/// by the same binding-power rule against the parent: `(a IS NULL) = b` wraps
2165/// (equal-precedence, non-associative), while `a IS NULL AND b` stays bare.
2166fn binary_child_needs_parens<X: Extension + Render>(
2167    bp: &BindingPowerTable,
2168    parent: &BinaryOperator,
2169    child: &Expr<X>,
2170    side: Side,
2171) -> bool {
2172    match child {
2173        Expr::BinaryOp { op, .. } => bp.needs_parens(parent, op, side),
2174        Expr::UnaryOp { op, .. } => match side {
2175            Side::Right => false,
2176            Side::Left => bp.prefix(op) < bp.binary(parent).left,
2177        },
2178        Expr::Between { .. }
2179        | Expr::Like { .. }
2180        | Expr::QuantifiedLike { .. }
2181        | Expr::InList { .. }
2182        | Expr::InSubquery { .. } => {
2183            needs_parens_between(bp.binary(parent), bp.range_predicate(), side)
2184        }
2185        Expr::IsNull { .. } | Expr::IsTruth { .. } | Expr::IsNormalized { .. } => {
2186            needs_parens_between(bp.binary(parent), bp.predicate(), side)
2187        }
2188        // A quantified comparison (`x = ANY (…)`) binds at its comparison operator's rank, not
2189        // the `IS`-family predicate tier — the parser climbs it at `binding_power(op)`.
2190        Expr::QuantifiedComparison { .. } => {
2191            needs_parens_between(bp.binary(parent), bp.comparison, side)
2192        }
2193        Expr::InExpr { .. } => {
2194            needs_parens_between(bp.binary(parent), UNPARENTHESIZED_IN_LIST, side)
2195        }
2196        Expr::NamedOperator { .. } => {
2197            needs_parens_between(bp.binary(parent), bp.any_operator, side)
2198        }
2199        Expr::PrefixOperator { .. } => match side {
2200            Side::Right => false,
2201            Side::Left => bp.any_operator.left < bp.binary(parent).left,
2202        },
2203        // A postfix operator is the mirror of a prefix operator: it closes its outer-left
2204        // edge with its operand, so it needs parens only as a looser-binding *right* operand.
2205        Expr::PostfixOperator { .. } => match side {
2206            Side::Left => false,
2207            Side::Right => bp.any_operator.left < bp.binary(parent).right,
2208        },
2209        // A lambda binds at the JSON-arrow rank (it is that token), so it groups as
2210        // a `JsonGet` child of the parent operator.
2211        Expr::Lambda { .. } => {
2212            needs_parens_between(bp.binary(parent), bp.binary(&BinaryOperator::JsonGet), side)
2213        }
2214        Expr::Other { ext, .. } => match ext.operand_binding_power() {
2215            Some(child) => needs_parens_between(bp.binary(parent), child, side),
2216            None => false,
2217        },
2218        _ => false,
2219    }
2220}
2221
2222/// Whether a prefix operator's operand needs parens.
2223///
2224/// A binary operand needs parens when it binds looser than the prefix on the side
2225/// the prefix reaches across (`NOT (a AND b)`, `-(a + b)`), per the target
2226/// dialect's binding-power table. A nested sign operand is parenthesized so the
2227/// spelling does not collide with `--` / `++` tokens. A comparison-level predicate
2228/// operand binds at the predicate level, so it mirrors the binary arm against that
2229/// level: tighter-binding `-` wraps it (`-(a IS NULL)`, else `-a IS NULL` re-parses
2230/// as `(-a) IS NULL`), while looser `NOT` leaves `NOT a IS NULL` bare.
2231fn prefix_operand_needs_parens<X: Extension + Render>(
2232    bp: &BindingPowerTable,
2233    op: &UnaryOperator,
2234    operand: &Expr<X>,
2235) -> bool {
2236    match operand {
2237        Expr::BinaryOp { op: child, .. } => bp.binary(child).left < bp.prefix(op),
2238        Expr::UnaryOp { op: inner, .. } => is_sign(op) && is_sign(inner),
2239        Expr::Between { .. }
2240        | Expr::Like { .. }
2241        | Expr::QuantifiedLike { .. }
2242        | Expr::InList { .. }
2243        | Expr::InSubquery { .. } => bp.range_predicate().left < bp.prefix(op),
2244        Expr::IsNull { .. } | Expr::IsTruth { .. } | Expr::IsNormalized { .. } => {
2245            bp.predicate().left < bp.prefix(op)
2246        }
2247        // A quantified comparison binds at its comparison operator's rank (see
2248        // `binary_child_needs_parens`).
2249        Expr::QuantifiedComparison { .. } => bp.comparison.left < bp.prefix(op),
2250        Expr::InExpr { .. } => UNPARENTHESIZED_IN_LIST.left < bp.prefix(op),
2251        Expr::NamedOperator { .. } => bp.any_operator.left < bp.prefix(op),
2252        // A prefix symbolic operator binds at the "any other operator" rank; a tighter
2253        // sign prefix wraps it (so `-@a` renders `-(@a)`, never the `-@` operator).
2254        Expr::PrefixOperator { .. } => bp.any_operator.left < bp.prefix(op),
2255        // A postfix symbolic operator binds at the "any other operator" rank; a tighter sign
2256        // prefix wraps it (so `-(1!)` never renders `-1!`, which re-parses `(-1)!`).
2257        Expr::PostfixOperator { .. } => bp.any_operator.left < bp.prefix(op),
2258        // A lambda binds at the JSON-arrow rank, so a tighter prefix wraps it.
2259        Expr::Lambda { .. } => bp.binary(&BinaryOperator::JsonGet).left < bp.prefix(op),
2260        Expr::Other { ext, .. } => ext
2261            .operand_binding_power()
2262            .is_some_and(|child| child.left < bp.prefix(op)),
2263        _ => false,
2264    }
2265}
2266
2267fn is_sign(op: &UnaryOperator) -> bool {
2268    matches!(op, UnaryOperator::Minus | UnaryOperator::Plus)
2269}
2270
2271/// Render an operand of a PostgreSQL postfix operator (`::`, `[]`, `COLLATE`, the
2272/// left of `AT TIME ZONE`) or the `AT TIME ZONE` zone, adding the binding-power
2273/// parentheses `parent`/`side` demand.
2274fn render_pg_operand<X: Extension + Render>(
2275    child: &Expr<X>,
2276    parent: BindingPower,
2277    side: Side,
2278    ctx: &RenderCtx<'_>,
2279    f: &mut fmt::Formatter<'_>,
2280) -> fmt::Result {
2281    let canonical = operand_needs_parens(&ctx.target().binding_powers, parent, child, side);
2282    render_operand(child, canonical, ctx, f)
2283}
2284
2285/// Whether `child` needs parentheses as an operand of an operator whose binding
2286/// power is `parent`, on the given `side`.
2287///
2288/// Centralizes child-precedence derivation for every operand position keyed by a
2289/// parent [`BindingPower`] — the PostgreSQL postfix operators and the extension
2290/// infix operators ([`render_extension_infix`]). A child binding looser than the
2291/// side the parent reaches across re-associates away without parentheses and must
2292/// be wrapped; a prefix-operator child closes its inner edge, so it only
2293/// needs parens as a looser-binding *left* operand. An extension operator child
2294/// reports its own precedence via [`Render::operand_binding_power`], so a custom
2295/// operator nested in any operand position groups by the same rule. Atoms, calls,
2296/// `CAST(...)` calls, the prefixed typed string constant (`type 'string'`), and the
2297/// self-delimiting `ARRAY`/`ROW`/`[…]`/`{…}`/`MAP {…}` constructors never need them.
2298fn operand_needs_parens<X: Extension + Render>(
2299    bp: &BindingPowerTable,
2300    parent: BindingPower,
2301    child: &Expr<X>,
2302    side: Side,
2303) -> bool {
2304    let child_bp = match child {
2305        Expr::BinaryOp { op, .. } => bp.binary(op),
2306        Expr::UnaryOp { op, .. } => {
2307            return side == Side::Left && bp.prefix(op) < parent.left;
2308        }
2309        Expr::Between { .. }
2310        | Expr::Like { .. }
2311        | Expr::QuantifiedLike { .. }
2312        | Expr::InList { .. }
2313        | Expr::InSubquery { .. } => bp.range_predicate(),
2314        Expr::IsNull { .. } | Expr::IsTruth { .. } | Expr::IsNormalized { .. } => bp.predicate(),
2315        // A quantified comparison binds at its comparison operator's rank (see
2316        // `binary_child_needs_parens`).
2317        Expr::QuantifiedComparison { .. } => bp.comparison,
2318        Expr::InExpr { .. } => UNPARENTHESIZED_IN_LIST,
2319        Expr::Cast {
2320            syntax: CastSyntax::DoubleColon,
2321            ..
2322        } => bp.typecast,
2323        Expr::Subscript { .. } => bp.subscript,
2324        Expr::Collate { .. } => bp.collate,
2325        Expr::AtTimeZone { .. } => bp.at_time_zone,
2326        Expr::FieldSelection { .. } => bp.field_selection,
2327        // A named operator (bare `a ~ b` or `OPERATOR(...)`) binds at the "any other
2328        // operator" rank.
2329        Expr::NamedOperator { .. } => bp.any_operator,
2330        // A prefix symbolic operator closes its inner edge, so — like a unary op — it only
2331        // needs parens as a looser-binding left operand.
2332        Expr::PrefixOperator { .. } => {
2333            return side == Side::Left && bp.any_operator.left < parent.left;
2334        }
2335        // A postfix symbolic operator closes its outer-left edge with its operand (the mirror
2336        // of a prefix operator), so it only needs parens as a looser-binding right operand.
2337        Expr::PostfixOperator { .. } => {
2338            return side == Side::Right && bp.any_operator.left < parent.right;
2339        }
2340        // A lambda binds at the JSON-arrow rank (it is that token).
2341        Expr::Lambda { .. } => bp.binary(&BinaryOperator::JsonGet),
2342        Expr::Other { ext, .. } => match ext.operand_binding_power() {
2343            Some(child_bp) => child_bp,
2344            None => return false,
2345        },
2346        _ => return false,
2347    };
2348    needs_parens_between(parent, child_bp, side)
2349}
2350
2351// ---------------------------------------------------------------------------
2352// Extension operator rendering (the public ADR-0009 seam)
2353// ---------------------------------------------------------------------------
2354
2355/// Render a binary extension operator — `operands.0`, the operator token written by
2356/// `op`, then `operands.1` — with exactly the parentheses its binding power `bp`
2357/// requires, plus the full self-wrapping the `Parenthesized` oracle mode adds.
2358///
2359/// This is the blessed way an `Expr::Other` infix-operator node renders: it reuses
2360/// the same machinery the built-in [`Expr::BinaryOp`]
2361/// arm uses, so a custom-operator tree round-trips by the same binding-power rule.
2362/// `bp` MUST equal the binding power the dialect's
2363/// `peek_infix_operator_hook` reported and that the node returns from
2364/// [`Render::operand_binding_power`]. `op` writes the operator token with its own
2365/// surrounding spaces (e.g. `|f| f.write_str(" ~ ")`). `operands` is the `(left,
2366/// right)` pair, bundled so the signature stays within the argument budget.
2367pub fn render_extension_infix<X: Extension + Render>(
2368    ctx: &RenderCtx<'_>,
2369    f: &mut fmt::Formatter<'_>,
2370    bp: BindingPower,
2371    operands: (&Expr<X>, &Expr<X>),
2372    op: impl FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result,
2373) -> fmt::Result {
2374    let (left, right) = operands;
2375    let table = &ctx.target().binding_powers;
2376    let full = ctx.mode() == RenderMode::Parenthesized;
2377    open_group(full, f)?;
2378    render_operand(
2379        left,
2380        operand_needs_parens(table, bp, left, Side::Left),
2381        ctx,
2382        f,
2383    )?;
2384    op(f)?;
2385    render_operand(
2386        right,
2387        operand_needs_parens(table, bp, right, Side::Right),
2388        ctx,
2389        f,
2390    )?;
2391    close_group(full, f)
2392}
2393
2394/// Render a prefix extension operator — the operator token written by `op`, then
2395/// `operand` — with exactly the parentheses its prefix binding power `bp` requires,
2396/// plus the `Parenthesized` self-wrapping.
2397///
2398/// The prefix analogue of [`render_extension_infix`]; `bp` MUST equal the binding
2399/// power the dialect's `peek_prefix_operator_hook` reported.
2400pub fn render_extension_prefix<X: Extension + Render>(
2401    ctx: &RenderCtx<'_>,
2402    f: &mut fmt::Formatter<'_>,
2403    bp: u8,
2404    op: impl FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result,
2405    operand: &Expr<X>,
2406) -> fmt::Result {
2407    let full = ctx.mode() == RenderMode::Parenthesized;
2408    open_group(full, f)?;
2409    op(f)?;
2410    render_operand(
2411        operand,
2412        prefix_extension_operand_needs_parens(&ctx.target().binding_powers, bp, operand),
2413        ctx,
2414        f,
2415    )?;
2416    close_group(full, f)
2417}
2418
2419/// Render a postfix extension operator — `operand`, then the operator token written by
2420/// `op` — with exactly the parentheses its binding power `bp` requires, plus the
2421/// `Parenthesized` self-wrapping.
2422///
2423/// The postfix analogue of [`render_extension_prefix`]; `bp` MUST equal the operator's
2424/// "any other operator" binding power. The operand is a *left* operand at that rank — a
2425/// postfix operator is a complete unary token, so its operand groups exactly as the left
2426/// operand of a same-rank infix operator (`operand_needs_parens(..., Side::Left)`): a
2427/// tighter operand needs no parens (`a + b !` re-parses `(a + b)!`), a looser one is
2428/// wrapped (`(a = b)!`).
2429pub fn render_extension_postfix<X: Extension + Render>(
2430    ctx: &RenderCtx<'_>,
2431    f: &mut fmt::Formatter<'_>,
2432    bp: BindingPower,
2433    operand: &Expr<X>,
2434    op: impl FnOnce(&mut fmt::Formatter<'_>) -> fmt::Result,
2435) -> fmt::Result {
2436    let table = &ctx.target().binding_powers;
2437    let full = ctx.mode() == RenderMode::Parenthesized;
2438    open_group(full, f)?;
2439    render_operand(
2440        operand,
2441        operand_needs_parens(table, bp, operand, Side::Left),
2442        ctx,
2443        f,
2444    )?;
2445    op(f)?;
2446    close_group(full, f)
2447}
2448
2449/// Whether a prefix extension operator's operand needs parens, given the operator's
2450/// prefix binding power `bp`. A binary or comparison-level-predicate operand binding
2451/// looser than the prefix is wrapped; an extension operand reports its own
2452/// precedence; atoms and a nested unary operand (which closes its own edge) never
2453/// need them.
2454fn prefix_extension_operand_needs_parens<X: Extension + Render>(
2455    table: &BindingPowerTable,
2456    bp: u8,
2457    operand: &Expr<X>,
2458) -> bool {
2459    match operand {
2460        Expr::BinaryOp { op, .. } => table.binary(op).left < bp,
2461        Expr::IsNull { .. } | Expr::IsTruth { .. } | Expr::IsNormalized { .. } => {
2462            table.predicate().left < bp
2463        }
2464        // The range/pattern/membership family binds at the range-predicate rank; a quantified
2465        // comparison binds at its comparison operator's rank (parse-aligned, ADR-0008).
2466        Expr::Between { .. }
2467        | Expr::Like { .. }
2468        | Expr::QuantifiedLike { .. }
2469        | Expr::InList { .. }
2470        | Expr::InSubquery { .. } => table.range_predicate().left < bp,
2471        Expr::QuantifiedComparison { .. } => table.comparison.left < bp,
2472        Expr::InExpr { .. } => UNPARENTHESIZED_IN_LIST.left < bp,
2473        Expr::NamedOperator { .. } => table.any_operator.left < bp,
2474        Expr::PrefixOperator { .. } => table.any_operator.left < bp,
2475        Expr::PostfixOperator { .. } => table.any_operator.left < bp,
2476        // A lambda binds at the JSON-arrow rank, so a tighter prefix wraps it.
2477        Expr::Lambda { .. } => table.binary(&BinaryOperator::JsonGet).left < bp,
2478        Expr::Other { ext, .. } => ext
2479            .operand_binding_power()
2480            .is_some_and(|child| child.left < bp),
2481        _ => false,
2482    }
2483}
2484
2485// ---------------------------------------------------------------------------
2486// Statements and queries
2487// ---------------------------------------------------------------------------
2488
2489impl<X: Extension + Render> Render for Statement<X> {
2490    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2491        match self {
2492            Statement::Query { query, .. } => query.render(ctx, f),
2493            Statement::CreateTable { create, .. } => create.render(ctx, f),
2494            Statement::AlterTable { alter, .. } => alter.render(ctx, f),
2495            Statement::Drop { drop, .. } => drop.render(ctx, f),
2496            Statement::CreateSchema { schema, .. } => schema.render(ctx, f),
2497            Statement::CreateView { view, .. } => view.render(ctx, f),
2498            Statement::RefreshMaterializedView { refresh, .. } => refresh.render(ctx, f),
2499            Statement::CreateColocationGroup { create, .. } => create.render(ctx, f),
2500            Statement::DropColocationGroup { drop, .. } => drop.render(ctx, f),
2501            Statement::AlterView { alter, .. } => alter.render(ctx, f),
2502            Statement::CreateIndex { index, .. } => index.render(ctx, f),
2503            Statement::CreateFunction { create, .. } => create.render(ctx, f),
2504            Statement::CreateProcedure { create, .. } => create.render(ctx, f),
2505            Statement::AlterRoutine { alter, .. } => alter.render(ctx, f),
2506            Statement::CreateEvent { create, .. } => create.render(ctx, f),
2507            Statement::AlterEvent { alter, .. } => alter.render(ctx, f),
2508            Statement::DropEvent { drop, .. } => drop.render(ctx, f),
2509            Statement::DropDatabase { drop, .. } => drop.render(ctx, f),
2510            Statement::DropIndex { drop, .. } => drop.render(ctx, f),
2511            Statement::CreateDatabase { create, .. } => create.render(ctx, f),
2512            Statement::DropRoutine {
2513                kind,
2514                if_exists,
2515                routines,
2516                behavior,
2517                ..
2518            } => {
2519                f.write_str("DROP ")?;
2520                kind.render(ctx, f)?;
2521                if *if_exists {
2522                    f.write_str(" IF EXISTS")?;
2523                }
2524                f.write_str(" ")?;
2525                render_comma_separated(routines, ctx, f)?;
2526                render_drop_behavior(*behavior, ctx, f)
2527            }
2528            Statement::DropTransform { drop, .. } => drop.render(ctx, f),
2529            Statement::Truncate {
2530                tables,
2531                table_keyword,
2532                restart_identity,
2533                behavior,
2534                ..
2535            } => {
2536                // The optional `TABLE` keyword is exact-synonym sugar: the canonical form
2537                // always emits it, a source-fidelity render drops it when the source did.
2538                if *table_keyword || !honours_source_spelling(ctx) {
2539                    f.write_str("TRUNCATE TABLE ")?;
2540                } else {
2541                    f.write_str("TRUNCATE ")?;
2542                }
2543                render_comma_separated(tables, ctx, f)?;
2544                match restart_identity {
2545                    Some(true) => f.write_str(" RESTART IDENTITY")?,
2546                    Some(false) => f.write_str(" CONTINUE IDENTITY")?,
2547                    None => {}
2548                }
2549                render_drop_behavior(*behavior, ctx, f)
2550            }
2551            Statement::CommentOn { comment, .. } => {
2552                let CommentOnStatement {
2553                    if_exists,
2554                    target,
2555                    name,
2556                    constraint_table,
2557                    comment,
2558                    ..
2559                } = comment.as_ref();
2560                f.write_str("COMMENT ")?;
2561                if *if_exists {
2562                    f.write_str("IF EXISTS ")?;
2563                }
2564                f.write_str("ON ")?;
2565                f.write_str(match target {
2566                    CommentTarget::Table => "TABLE ",
2567                    CommentTarget::Column => "COLUMN ",
2568                    CommentTarget::Database => "DATABASE ",
2569                    CommentTarget::View => "VIEW ",
2570                    CommentTarget::MaterializedView => "MATERIALIZED VIEW ",
2571                    CommentTarget::Index => "INDEX ",
2572                    CommentTarget::Constraint => "CONSTRAINT ",
2573                    CommentTarget::Procedure { .. } => "PROCEDURE ",
2574                })?;
2575                name.render(ctx, f)?;
2576                if let Some(table) = constraint_table {
2577                    f.write_str(" ON ")?;
2578                    table.render(ctx, f)?;
2579                }
2580                if let CommentTarget::Procedure {
2581                    arg_types: Some(arg_types),
2582                } = target
2583                {
2584                    f.write_str("(")?;
2585                    render_comma_separated(arg_types, ctx, f)?;
2586                    f.write_str(")")?;
2587                }
2588                f.write_str(" IS ")?;
2589                match comment {
2590                    Some(literal) => literal.render(ctx, f)?,
2591                    None => f.write_str("NULL")?,
2592                }
2593                Ok(())
2594            }
2595            Statement::Insert { insert, .. } => insert.render(ctx, f),
2596            Statement::Update { update, .. } => update.render(ctx, f),
2597            Statement::Delete { delete, .. } => delete.render(ctx, f),
2598            Statement::Merge { merge, .. } => merge.render(ctx, f),
2599            Statement::Transaction { transaction, .. } => transaction.render(ctx, f),
2600            Statement::Xa { xa, .. } => xa.render(ctx, f),
2601            Statement::Session { session, .. } => session.render(ctx, f),
2602            Statement::AccessControl { access, .. } => access.render(ctx, f),
2603            Statement::Copy { copy, .. } => copy.render(ctx, f),
2604            Statement::CopyInto { copy, .. } => copy.render(ctx, f),
2605            Statement::Export { export, .. } => export.render(ctx, f),
2606            Statement::Import { import, .. } => import.render(ctx, f),
2607            Statement::Explain { explain, .. } => explain.render(ctx, f),
2608            Statement::Describe { describe, .. } => describe.render(ctx, f),
2609            Statement::Show { show, .. } => show.render(ctx, f),
2610            Statement::Kill { kill, .. } => kill.render(ctx, f),
2611            Statement::Handler { handler, .. } => handler.render(ctx, f),
2612            Statement::Install { install, .. } => install.render(ctx, f),
2613            Statement::Uninstall { uninstall, .. } => uninstall.render(ctx, f),
2614            Statement::Shutdown { .. } => f.write_str("SHUTDOWN"),
2615            Statement::Restart { .. } => f.write_str("RESTART"),
2616            Statement::Clone { clone, .. } => clone.render(ctx, f),
2617            Statement::ImportTable { import_table, .. } => import_table.render(ctx, f),
2618            Statement::Help { help, .. } => help.render(ctx, f),
2619            Statement::Binlog { binlog, .. } => binlog.render(ctx, f),
2620            Statement::Pragma { pragma, .. } => pragma.render(ctx, f),
2621            Statement::Attach { attach, .. } => attach.render(ctx, f),
2622            Statement::Detach { detach, .. } => detach.render(ctx, f),
2623            Statement::Checkpoint { checkpoint, .. } => checkpoint.render(ctx, f),
2624            Statement::Load { load, .. } => load.render(ctx, f),
2625            Statement::LoadData { load_data, .. } => load_data.render(ctx, f),
2626            Statement::UpdateExtensions {
2627                update_extensions, ..
2628            } => update_extensions.render(ctx, f),
2629            Statement::Vacuum { vacuum, .. } => vacuum.render(ctx, f),
2630            Statement::Reindex { reindex, .. } => reindex.render(ctx, f),
2631            Statement::Analyze { analyze, .. } => analyze.render(ctx, f),
2632            Statement::TableMaintenance {
2633                table_maintenance, ..
2634            } => table_maintenance.render(ctx, f),
2635            Statement::CacheIndex { cache_index, .. } => cache_index.render(ctx, f),
2636            Statement::LoadIndex { load_index, .. } => load_index.render(ctx, f),
2637            Statement::Rename { rename, .. } => rename.render(ctx, f),
2638            Statement::Flush { flush, .. } => flush.render(ctx, f),
2639            Statement::Purge { purge, .. } => purge.render(ctx, f),
2640            Statement::Replication { replication, .. } => replication.render(ctx, f),
2641            Statement::CreateUser { create, .. } => create.render(ctx, f),
2642            Statement::AlterUser { alter, .. } => alter.render(ctx, f),
2643            Statement::UserRoleList { statement, .. } => statement.render(ctx, f),
2644            Statement::Use { use_statement, .. } => use_statement.render(ctx, f),
2645            Statement::CreateTrigger { create, .. } => create.render(ctx, f),
2646            Statement::CreateStoredTrigger { create, .. } => create.render(ctx, f),
2647            Statement::CreateMacro { create, .. } => create.render(ctx, f),
2648            Statement::CreateSecret { create, .. } => create.render(ctx, f),
2649            Statement::DropSecret { drop, .. } => drop.render(ctx, f),
2650            Statement::CreateType { create, .. } => create.render(ctx, f),
2651            Statement::CreateVirtualTable { create, .. } => create.render(ctx, f),
2652            Statement::CreateSequence { create, .. } => create.render(ctx, f),
2653            Statement::CreateExtension { create, .. } => create.render(ctx, f),
2654            Statement::AlterExtension { alter, .. } => alter.render(ctx, f),
2655            Statement::CreateTablespace { create, .. } => create.render(ctx, f),
2656            Statement::AlterTablespace { alter, .. } => alter.render(ctx, f),
2657            Statement::DropTablespace { drop, .. } => drop.render(ctx, f),
2658            Statement::CreateLogfileGroup { create, .. } => create.render(ctx, f),
2659            Statement::AlterLogfileGroup { alter, .. } => alter.render(ctx, f),
2660            Statement::DropLogfileGroup { drop, .. } => drop.render(ctx, f),
2661            Statement::AlterObjectDepends { alter, .. } => alter.render(ctx, f),
2662            Statement::AlterSystem { alter, .. } => alter.render(ctx, f),
2663            Statement::AlterDatabase { alter, .. } => alter.render(ctx, f),
2664            Statement::AlterDatabaseOptions { alter, .. } => alter.render(ctx, f),
2665            Statement::CreateServer { create, .. } => create.render(ctx, f),
2666            Statement::AlterServer { alter, .. } => alter.render(ctx, f),
2667            Statement::DropServer { drop, .. } => drop.render(ctx, f),
2668            Statement::AlterInstance { alter, .. } => alter.render(ctx, f),
2669            Statement::CreateSpatialReferenceSystem { create, .. } => create.render(ctx, f),
2670            Statement::DropSpatialReferenceSystem { drop, .. } => drop.render(ctx, f),
2671            Statement::CreateResourceGroup { create, .. } => create.render(ctx, f),
2672            Statement::AlterResourceGroup { alter, .. } => alter.render(ctx, f),
2673            Statement::DropResourceGroup { drop, .. } => drop.render(ctx, f),
2674            Statement::AlterSequence { alter, .. } => alter.render(ctx, f),
2675            Statement::AlterObjectSchema { alter, .. } => alter.render(ctx, f),
2676            Statement::Pivot { pivot, .. } => pivot.render(ctx, f),
2677            Statement::Unpivot { unpivot, .. } => unpivot.render(ctx, f),
2678            // The statement form is the bare `SHOW_REF` (`DESCRIBE …` / `SUMMARIZE …`),
2679            // unparenthesized — the `ShowRef` core already renders the keyword + target; the
2680            // parentheses are the table-factor position's alone.
2681            Statement::ShowRef { show, .. } => show.render(ctx, f),
2682            Statement::Prepare { prepare, .. } => prepare.render(ctx, f),
2683            Statement::Execute { execute, .. } => execute.render(ctx, f),
2684            Statement::PrepareFrom { prepare_from, .. } => prepare_from.render(ctx, f),
2685            Statement::ExecuteUsing { execute_using, .. } => execute_using.render(ctx, f),
2686            Statement::Deallocate { deallocate, .. } => deallocate.render(ctx, f),
2687            Statement::Call { call, .. } => call.render(ctx, f),
2688            Statement::Do { do_block, .. } => do_block.render(ctx, f),
2689            Statement::DoExpressions { do_expressions, .. } => do_expressions.render(ctx, f),
2690            Statement::LockTables { lock_tables, .. } => lock_tables.render(ctx, f),
2691            Statement::UnlockTables { unlock_tables, .. } => unlock_tables.render(ctx, f),
2692            Statement::InstanceLock { instance_lock, .. } => instance_lock.render(ctx, f),
2693            Statement::Compound { compound, .. } => compound.render(ctx, f),
2694            Statement::If { if_statement, .. } => if_statement.render(ctx, f),
2695            Statement::Case { case_statement, .. } => case_statement.render(ctx, f),
2696            Statement::Loop { loop_statement, .. } => loop_statement.render(ctx, f),
2697            Statement::While {
2698                while_statement, ..
2699            } => while_statement.render(ctx, f),
2700            Statement::Repeat { repeat, .. } => repeat.render(ctx, f),
2701            Statement::Leave { leave, .. } => leave.render(ctx, f),
2702            Statement::Iterate { iterate, .. } => iterate.render(ctx, f),
2703            Statement::Return {
2704                return_statement, ..
2705            } => return_statement.render(ctx, f),
2706            Statement::OpenCursor { open, .. } => open.render(ctx, f),
2707            Statement::FetchCursor { fetch, .. } => fetch.render(ctx, f),
2708            Statement::CloseCursor { close, .. } => close.render(ctx, f),
2709            Statement::Signal { signal, .. } => signal.render_as(ctx, f, "SIGNAL"),
2710            Statement::Resignal { resignal, .. } => resignal.render_as(ctx, f, "RESIGNAL"),
2711            Statement::GetDiagnostics {
2712                get_diagnostics, ..
2713            } => get_diagnostics.render(ctx, f),
2714            Statement::Other { ext, .. } => ext.render(ctx, f),
2715        }
2716    }
2717}
2718
2719impl Render for RefreshMaterializedView {
2720    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2721        f.write_str("REFRESH MATERIALIZED VIEW")?;
2722        if self.concurrently {
2723            f.write_str(" CONCURRENTLY")?;
2724        }
2725        f.write_str(" ")?;
2726        self.name.render(ctx, f)?;
2727        match self.with_data {
2728            Some(true) => f.write_str(" WITH DATA"),
2729            Some(false) => f.write_str(" WITH NO DATA"),
2730            None => Ok(()),
2731        }
2732    }
2733}
2734
2735impl Render for CreateColocationGroup {
2736    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2737        f.write_str("CREATE COLOCATION GROUP")?;
2738        if self.if_not_exists {
2739            f.write_str(" IF NOT EXISTS")?;
2740        }
2741        f.write_str(" ")?;
2742        self.name.render(ctx, f)?;
2743        f.write_str(match self.partition {
2744            ColocationPartitionKind::Hash => " PARTITION BY HASH (",
2745            ColocationPartitionKind::Range => " PARTITION BY RANGE (",
2746        })?;
2747        render_ident_list(&self.columns, ctx, f)?;
2748        f.write_str(") SHARDS ")?;
2749        self.shards.render(ctx, f)
2750    }
2751}
2752
2753impl Render for DropColocationGroup {
2754    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2755        f.write_str("DROP COLOCATION GROUP")?;
2756        if self.if_exists {
2757            f.write_str(" IF EXISTS")?;
2758        }
2759        f.write_str(" ")?;
2760        self.name.render(ctx, f)
2761    }
2762}
2763
2764impl<X: Extension + Render> Render for Insert<X> {
2765    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2766        if let Some(with) = &self.with {
2767            with.render(ctx, f)?;
2768            f.write_str(" ")?;
2769        }
2770        // `INTO` is mandatory after `INSERT` and rendered canonically after `REPLACE`
2771        // too (the MySQL shorthand `REPLACE t` parses but normalizes to `REPLACE INTO`,
2772        // matching how a bare DML alias normalizes to the `AS` spelling). The SQLite
2773        // `OR <action>` conflict prefix sits between the verb keyword and `INTO`; the
2774        // parser only sets it on the `INSERT` spelling (`REPLACE` takes none).
2775        f.write_str(match self.verb {
2776            InsertVerb::Insert => "INSERT",
2777            InsertVerb::Replace => "REPLACE",
2778        })?;
2779        if let Some(modifier) = self.modifier {
2780            f.write_str(match modifier {
2781                InsertModifier::Ignore => " IGNORE",
2782                InsertModifier::Overwrite => " OVERWRITE",
2783            })?;
2784        }
2785        if let Some(or_action) = self.or_action {
2786            f.write_str(" OR ")?;
2787            or_action.render(ctx, f)?;
2788        }
2789        f.write_str(" INTO ")?;
2790        self.target.render(ctx, f)?;
2791        if let Some(mode) = self.column_matching {
2792            f.write_str(match mode {
2793                InsertColumnMatching::ByName => " BY NAME",
2794                InsertColumnMatching::ByPosition => " BY POSITION",
2795            })?;
2796        }
2797        if let Some(overriding) = self.overriding {
2798            f.write_str(" ")?;
2799            overriding.render(ctx, f)?;
2800        }
2801        f.write_str(" ")?;
2802        self.source.render(ctx, f)?;
2803        // The MySQL row alias sits between the source and the `ON DUPLICATE KEY UPDATE`
2804        // clause; `render_table_alias` writes the leading ` AS `.
2805        render_table_alias(self.row_alias.as_ref(), ctx, f)?;
2806        if let Some(upsert) = &self.upsert {
2807            f.write_str(" ")?;
2808            upsert.render(ctx, f)?;
2809        }
2810        render_returning_clause(self.returning.as_ref(), ctx, f)
2811    }
2812}
2813
2814impl Render for InsertTarget {
2815    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2816        self.name.render(ctx, f)?;
2817        render_alias(self.alias.as_ref(), self.alias_spelling, ctx, f)?;
2818        if !self.columns.is_empty() {
2819            f.write_str(" (")?;
2820            render_ident_list(&self.columns, ctx, f)?;
2821            f.write_str(")")?;
2822        }
2823        Ok(())
2824    }
2825}
2826
2827impl Render for InsertOverriding {
2828    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2829        f.write_str(match self {
2830            Self::SystemValue => "OVERRIDING SYSTEM VALUE",
2831            Self::UserValue => "OVERRIDING USER VALUE",
2832        })
2833    }
2834}
2835
2836impl Render for ConflictResolution {
2837    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2838        f.write_str(match self {
2839            Self::Rollback => "ROLLBACK",
2840            Self::Abort => "ABORT",
2841            Self::Fail => "FAIL",
2842            Self::Ignore => "IGNORE",
2843            Self::Replace => "REPLACE",
2844        })
2845    }
2846}
2847
2848impl<X: Extension + Render> Render for InsertSource<X> {
2849    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2850        match self {
2851            Self::DefaultValues { default, .. } => {
2852                default.render(ctx, f)?;
2853                f.write_str(" VALUES")
2854            }
2855            Self::Values { values, .. } => values.render(ctx, f),
2856            Self::Query { query, .. } => query.render(ctx, f),
2857            Self::Set { assignments, .. } => {
2858                f.write_str("SET ")?;
2859                render_update_assignments(assignments, ctx, f)
2860            }
2861        }
2862    }
2863}
2864
2865impl<X: Extension + Render> Render for InsertValues<X> {
2866    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2867        // The `INSERT ... VALUES` source list uses bare `( ... )` rows on every dialect.
2868        f.write_str("VALUES ")?;
2869        render_values_rows(&self.rows, false, ctx, f)
2870    }
2871}
2872
2873impl<X: Extension + Render> Render for InsertValue<X> {
2874    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2875        match self {
2876            Self::Expr { expr, .. } => expr.render(ctx, f),
2877            Self::Default { default, .. } => default.render(ctx, f),
2878        }
2879    }
2880}
2881
2882impl Render for DefaultValue {
2883    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2884        f.write_str("DEFAULT")
2885    }
2886}
2887
2888impl Render for DmlTarget {
2889    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2890        render_relation_inheritance(&self.inheritance, &self.name, ctx, f)?;
2891        render_alias(self.alias.as_ref(), self.alias_spelling, ctx, f)
2892    }
2893}
2894
2895impl<X: Extension + Render> Render for Update<X> {
2896    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2897        if let Some(with) = &self.with {
2898            with.render(ctx, f)?;
2899            f.write_str(" ")?;
2900        }
2901        f.write_str("UPDATE")?;
2902        if let Some(or_action) = self.or_action {
2903            f.write_str(" OR ")?;
2904            or_action.render(ctx, f)?;
2905        }
2906        f.write_str(" ")?;
2907        self.target.render(ctx, f)?;
2908        for join in &self.target_joins {
2909            f.write_str(" ")?;
2910            join.render(ctx, f)?;
2911        }
2912        f.write_str(" SET ")?;
2913        render_update_assignments(&self.assignments, ctx, f)?;
2914        if !self.from.is_empty() {
2915            f.write_str(" FROM ")?;
2916            render_comma_separated(&self.from, ctx, f)?;
2917        }
2918        render_dml_selection(self.selection.as_ref(), ctx, f)?;
2919        render_mutation_order_by_limit(&self.order_by, self.limit.as_ref(), ctx, f)?;
2920        render_returning_clause(self.returning.as_ref(), ctx, f)
2921    }
2922}
2923
2924impl<X: Extension + Render> Render for UpdateAssignment<X> {
2925    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2926        match self {
2927            Self::Single { target, value, .. } => {
2928                target.render(ctx, f)?;
2929                f.write_str(" = ")?;
2930                value.render(ctx, f)
2931            }
2932            Self::Tuple {
2933                targets, source, ..
2934            } => {
2935                f.write_str("(")?;
2936                render_comma_separated(targets, ctx, f)?;
2937                f.write_str(") = ")?;
2938                source.render(ctx, f)
2939            }
2940        }
2941    }
2942}
2943
2944impl<X: Extension + Render> Render for UpdateValue<X> {
2945    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2946        match self {
2947            Self::Expr { expr, .. } => expr.render(ctx, f),
2948            Self::Default { default, .. } => default.render(ctx, f),
2949        }
2950    }
2951}
2952
2953impl<X: Extension + Render> Render for UpdateTupleSource<X> {
2954    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2955        match self {
2956            Self::Row {
2957                explicit, values, ..
2958            } => {
2959                f.write_str(if *explicit { "ROW(" } else { "(" })?;
2960                render_comma_separated(values, ctx, f)?;
2961                f.write_str(")")
2962            }
2963            Self::Subquery { query, .. } => {
2964                f.write_str("(")?;
2965                query.render(ctx, f)?;
2966                f.write_str(")")
2967            }
2968            Self::Default { default, .. } => default.render(ctx, f),
2969        }
2970    }
2971}
2972
2973impl<X: Extension + Render> Render for Delete<X> {
2974    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2975        if let Some(with) = &self.with {
2976            with.render(ctx, f)?;
2977            f.write_str(" ")?;
2978        }
2979        f.write_str("DELETE FROM ")?;
2980        self.target.render(ctx, f)?;
2981        for target in &self.additional_targets {
2982            f.write_str(", ")?;
2983            target.render(ctx, f)?;
2984        }
2985        for join in &self.target_joins {
2986            f.write_str(" ")?;
2987            join.render(ctx, f)?;
2988        }
2989        if !self.using.is_empty() {
2990            f.write_str(" USING ")?;
2991            render_comma_separated(&self.using, ctx, f)?;
2992        }
2993        render_dml_selection(self.selection.as_ref(), ctx, f)?;
2994        render_mutation_order_by_limit(&self.order_by, self.limit.as_ref(), ctx, f)?;
2995        render_returning_clause(self.returning.as_ref(), ctx, f)
2996    }
2997}
2998
2999/// Render a trailing ` WHERE ...` filter when present; the leading space is part of
3000/// the clause separator, mirroring the other mutation tails.
3001/// Render a non-empty comma-separated `UpdateAssignment` list — the shared `SET`
3002/// body of an `UPDATE`, a PostgreSQL `ON CONFLICT DO UPDATE`, and a MySQL
3003/// `ON DUPLICATE KEY UPDATE`. The `SET` keyword (and any clause lead-in) is the
3004/// caller's, so the three spellings differ only in their prefix, not this body.
3005fn render_update_assignments<X: Extension + Render>(
3006    assignments: &[UpdateAssignment<X>],
3007    ctx: &RenderCtx<'_>,
3008    f: &mut fmt::Formatter<'_>,
3009) -> fmt::Result {
3010    for (i, assignment) in assignments.iter().enumerate() {
3011        if i > 0 {
3012            f.write_str(", ")?;
3013        }
3014        assignment.render(ctx, f)?;
3015    }
3016    Ok(())
3017}
3018
3019fn render_dml_selection<X: Extension + Render>(
3020    selection: Option<&DmlSelection<X>>,
3021    ctx: &RenderCtx<'_>,
3022    f: &mut fmt::Formatter<'_>,
3023) -> fmt::Result {
3024    if let Some(selection) = selection {
3025        f.write_str(" WHERE ")?;
3026        selection.render(ctx, f)?;
3027    }
3028    Ok(())
3029}
3030
3031impl<X: Extension + Render> Render for DmlSelection<X> {
3032    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3033        match self {
3034            Self::Where { condition, .. } => condition.render(ctx, f),
3035            Self::CurrentOf { cursor, .. } => {
3036                f.write_str("CURRENT OF ")?;
3037                cursor.render(ctx, f)
3038            }
3039        }
3040    }
3041}
3042
3043/// Render the MySQL `[ORDER BY <keys>] [LIMIT <count>]` row-limiting tails shared by a
3044/// single-table `UPDATE` and `DELETE`; each leading space is part of the clause
3045/// separator, mirroring [`Query`]'s `ORDER BY` / `LIMIT` rendering.
3046fn render_mutation_order_by_limit<X: Extension + Render>(
3047    order_by: &[OrderByExpr<X>],
3048    limit: Option<&Limit<X>>,
3049    ctx: &RenderCtx<'_>,
3050    f: &mut fmt::Formatter<'_>,
3051) -> fmt::Result {
3052    if !order_by.is_empty() {
3053        f.write_str(" ORDER BY ")?;
3054        render_comma_separated(order_by, ctx, f)?;
3055    }
3056    if let Some(limit) = limit {
3057        f.write_str(" ")?;
3058        limit.render(ctx, f)?;
3059    }
3060    Ok(())
3061}
3062
3063/// Render a trailing ` RETURNING <output> [, ...]` clause when present; the leading
3064/// space is part of the clause separator, mirroring the other mutation tails.
3065fn render_returning_clause<X: Extension + Render>(
3066    returning: Option<&Returning<X>>,
3067    ctx: &RenderCtx<'_>,
3068    f: &mut fmt::Formatter<'_>,
3069) -> fmt::Result {
3070    if let Some(returning) = returning {
3071        f.write_str(" ")?;
3072        returning.render(ctx, f)?;
3073    }
3074    Ok(())
3075}
3076
3077impl<X: Extension + Render> Render for Returning<X> {
3078    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3079        f.write_str("RETURNING ")?;
3080        render_comma_separated(&self.items, ctx, f)
3081    }
3082}
3083
3084impl<X: Extension + Render> Render for Upsert<X> {
3085    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3086        match self {
3087            Upsert::OnConflict { conflict, .. } => conflict.render(ctx, f),
3088            Upsert::OnDuplicateKeyUpdate { assignments, .. } => {
3089                f.write_str("ON DUPLICATE KEY UPDATE ")?;
3090                render_update_assignments(assignments, ctx, f)
3091            }
3092        }
3093    }
3094}
3095
3096impl<X: Extension + Render> Render for OnConflict<X> {
3097    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3098        f.write_str("ON CONFLICT")?;
3099        if let Some(target) = &self.target {
3100            f.write_str(" ")?;
3101            target.render(ctx, f)?;
3102        }
3103        f.write_str(" ")?;
3104        self.action.render(ctx, f)
3105    }
3106}
3107
3108impl<X: Extension + Render> Render for ConflictTarget<X> {
3109    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3110        match self {
3111            ConflictTarget::Index {
3112                columns, predicate, ..
3113            } => {
3114                f.write_str("(")?;
3115                render_comma_separated(columns, ctx, f)?;
3116                f.write_str(")")?;
3117                if let Some(predicate) = predicate {
3118                    f.write_str(" WHERE ")?;
3119                    predicate.render(ctx, f)?;
3120                }
3121                Ok(())
3122            }
3123            ConflictTarget::Constraint { name, .. } => {
3124                f.write_str("ON CONSTRAINT ")?;
3125                name.render(ctx, f)
3126            }
3127        }
3128    }
3129}
3130
3131impl<X: Extension + Render> Render for ConflictAction<X> {
3132    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3133        match self {
3134            ConflictAction::Nothing { .. } => f.write_str("DO NOTHING"),
3135            ConflictAction::Update {
3136                assignments,
3137                selection,
3138                ..
3139            } => {
3140                f.write_str("DO UPDATE SET ")?;
3141                render_update_assignments(assignments, ctx, f)?;
3142                if let Some(selection) = selection {
3143                    f.write_str(" WHERE ")?;
3144                    selection.render(ctx, f)?;
3145                }
3146                Ok(())
3147            }
3148        }
3149    }
3150}
3151
3152impl<X: Extension + Render> Render for Merge<X> {
3153    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3154        if let Some(with) = &self.with {
3155            with.render(ctx, f)?;
3156            f.write_str(" ")?;
3157        }
3158        f.write_str("MERGE INTO ")?;
3159        self.target.render(ctx, f)?;
3160        f.write_str(" USING ")?;
3161        self.using.render(ctx, f)?;
3162        f.write_str(" ON ")?;
3163        self.on.render(ctx, f)?;
3164        for clause in &self.clauses {
3165            f.write_str(" ")?;
3166            clause.render(ctx, f)?;
3167        }
3168        render_returning_clause(self.returning.as_ref(), ctx, f)
3169    }
3170}
3171
3172impl<X: Extension + Render> Render for MergeWhenClause<X> {
3173    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3174        // `NotMatchedByTarget` renders the canonical bare spelling — `BY TARGET` is
3175        // the same production (both fold at parse), so the qualifier only appears
3176        // where it is load-bearing (`BY SOURCE`).
3177        f.write_str(match self.match_kind {
3178            MergeMatchKind::Matched => "WHEN MATCHED",
3179            MergeMatchKind::NotMatchedByTarget => "WHEN NOT MATCHED",
3180            MergeMatchKind::NotMatchedBySource => "WHEN NOT MATCHED BY SOURCE",
3181        })?;
3182        if let Some(condition) = &self.condition {
3183            f.write_str(" AND ")?;
3184            condition.render(ctx, f)?;
3185        }
3186        f.write_str(" THEN ")?;
3187        self.action.render(ctx, f)
3188    }
3189}
3190
3191impl<X: Extension + Render> Render for MergeAction<X> {
3192    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3193        match self {
3194            MergeAction::Insert {
3195                columns,
3196                overriding,
3197                values,
3198                additional_rows,
3199                ..
3200            } => {
3201                f.write_str("INSERT")?;
3202                if !columns.is_empty() {
3203                    f.write_str(" (")?;
3204                    render_ident_list(columns, ctx, f)?;
3205                    f.write_str(")")?;
3206                }
3207                if let Some(overriding) = overriding {
3208                    f.write_str(" ")?;
3209                    overriding.render(ctx, f)?;
3210                }
3211                f.write_str(" VALUES (")?;
3212                render_comma_separated(values, ctx, f)?;
3213                f.write_str(")")?;
3214                for row in additional_rows {
3215                    f.write_str(", (")?;
3216                    render_comma_separated(row, ctx, f)?;
3217                    f.write_str(")")?;
3218                }
3219                Ok(())
3220            }
3221            MergeAction::InsertDefault { default, .. } => {
3222                f.write_str("INSERT ")?;
3223                default.render(ctx, f)?;
3224                f.write_str(" VALUES")
3225            }
3226            MergeAction::Update { assignments, .. } => {
3227                f.write_str("UPDATE SET ")?;
3228                render_update_assignments(assignments, ctx, f)
3229            }
3230            MergeAction::UpdateStar { .. } => f.write_str("UPDATE SET *"),
3231            MergeAction::InsertStar { .. } => f.write_str("INSERT *"),
3232            MergeAction::InsertByName { star, .. } => {
3233                f.write_str("INSERT BY NAME")?;
3234                if *star {
3235                    f.write_str(" *")?;
3236                }
3237                Ok(())
3238            }
3239            MergeAction::Error { .. } => f.write_str("ERROR"),
3240            MergeAction::Delete { .. } => f.write_str("DELETE"),
3241            MergeAction::DoNothing { .. } => f.write_str("DO NOTHING"),
3242        }
3243    }
3244}
3245
3246impl Render for TransactionStatement {
3247    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3248        match self {
3249            Self::Begin {
3250                syntax,
3251                mode,
3252                block,
3253                name,
3254                modes,
3255                ..
3256            } => {
3257                match syntax {
3258                    TransactionStart::Begin => f.write_str("BEGIN")?,
3259                    TransactionStart::Start => {
3260                        f.write_str("START")?;
3261                        if honours_source_spelling(ctx) {
3262                            render_transaction_block_keyword(*block, f)?;
3263                        } else {
3264                            f.write_str(" TRANSACTION")?;
3265                        }
3266                    }
3267                }
3268                if let Some(mode) = mode {
3269                    f.write_str(" ")?;
3270                    mode.render(ctx, f)?;
3271                }
3272                // The `TRANSACTION`/`WORK` block noise word is exact-synonym fidelity —
3273                // replayed only by a source-fidelity render, dropped by a re-spell and
3274                // the redacted fingerprint. The START spelling handles its block word
3275                // above because its standard re-spell must restore `TRANSACTION`.
3276                if *syntax == TransactionStart::Begin {
3277                    render_transaction_block_and_name(*block, name.as_deref(), ctx, f)?;
3278                }
3279                render_transaction_modes(modes, ctx, f)
3280            }
3281            Self::Commit {
3282                syntax,
3283                block,
3284                name,
3285                chain,
3286                release,
3287                ..
3288            } => {
3289                if honours_source_spelling(ctx) && *syntax == TransactionCommitKeyword::End {
3290                    f.write_str("END")?;
3291                } else {
3292                    f.write_str("COMMIT")?;
3293                }
3294                render_transaction_block_and_name(*block, name.as_deref(), ctx, f)?;
3295                render_transaction_completion(*chain, *release, f)
3296            }
3297            Self::Rollback {
3298                syntax,
3299                block,
3300                name,
3301                savepoint_keyword,
3302                to_savepoint,
3303                chain,
3304                release,
3305                ..
3306            } => {
3307                if honours_source_spelling(ctx) && *syntax == TransactionRollbackKeyword::Abort {
3308                    f.write_str("ABORT")?;
3309                } else {
3310                    f.write_str("ROLLBACK")?;
3311                }
3312                render_transaction_block_and_name(*block, name.as_deref(), ctx, f)?;
3313                if let Some(name) = to_savepoint {
3314                    // The `SAVEPOINT` keyword is optional after `TO`; the canonical
3315                    // render emits it, a source-fidelity render drops it when the
3316                    // source did (`ROLLBACK TO x`).
3317                    if *savepoint_keyword || !honours_source_spelling(ctx) {
3318                        f.write_str(" TO SAVEPOINT ")?;
3319                    } else {
3320                        f.write_str(" TO ")?;
3321                    }
3322                    name.render(ctx, f)?;
3323                }
3324                render_transaction_completion(*chain, *release, f)
3325            }
3326            Self::Savepoint { name, .. } => {
3327                f.write_str("SAVEPOINT ")?;
3328                name.render(ctx, f)
3329            }
3330            Self::Release {
3331                savepoint_keyword,
3332                savepoint,
3333                ..
3334            } => {
3335                // The `SAVEPOINT` keyword is optional; the canonical render emits it, a
3336                // source-fidelity render drops it when the source did (`RELEASE x`).
3337                if *savepoint_keyword || !honours_source_spelling(ctx) {
3338                    f.write_str("RELEASE SAVEPOINT ")?;
3339                } else {
3340                    f.write_str("RELEASE ")?;
3341                }
3342                savepoint.render(ctx, f)
3343            }
3344            Self::SetCharacteristics { modes, .. } => {
3345                f.write_str("SET TRANSACTION")?;
3346                render_transaction_modes(modes, ctx, f)
3347            }
3348        }
3349    }
3350}
3351
3352fn render_transaction_chain(chain: Option<bool>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3353    match chain {
3354        Some(true) => f.write_str(" AND CHAIN"),
3355        Some(false) => f.write_str(" AND NO CHAIN"),
3356        None => Ok(()),
3357    }
3358}
3359
3360fn render_transaction_completion(
3361    chain: Option<bool>,
3362    release: Option<bool>,
3363    f: &mut fmt::Formatter<'_>,
3364) -> fmt::Result {
3365    render_transaction_chain(chain, f)?;
3366    match release {
3367        Some(true) => f.write_str(" RELEASE"),
3368        Some(false) => f.write_str(" NO RELEASE"),
3369        None => Ok(()),
3370    }
3371}
3372
3373/// Render the optional `TRANSACTION` / `WORK` block noise word (with its leading
3374/// space) after `BEGIN` / `COMMIT` / `ROLLBACK`; a `None` tag renders nothing. Only a
3375/// source-fidelity render calls this — the callers already gate on
3376/// [`honours_source_spelling`].
3377fn render_transaction_block_keyword(
3378    block: Option<TransactionBlockKeyword>,
3379    f: &mut fmt::Formatter<'_>,
3380) -> fmt::Result {
3381    match block {
3382        Some(TransactionBlockKeyword::Transaction) => f.write_str(" TRANSACTION"),
3383        Some(TransactionBlockKeyword::Work) => f.write_str(" WORK"),
3384        None => Ok(()),
3385    }
3386}
3387
3388fn render_transaction_block_and_name(
3389    block: Option<TransactionBlockKeyword>,
3390    name: Option<&Ident>,
3391    ctx: &RenderCtx<'_>,
3392    f: &mut fmt::Formatter<'_>,
3393) -> fmt::Result {
3394    if let Some(name) = name {
3395        f.write_str(" TRANSACTION ")?;
3396        name.render(ctx, f)
3397    } else if honours_source_spelling(ctx) {
3398        render_transaction_block_keyword(block, f)
3399    } else {
3400        Ok(())
3401    }
3402}
3403
3404/// Render a transaction mode list, each mode preceded by its separator (a leading
3405/// space for the first, `, ` thereafter), so an empty list renders nothing.
3406fn render_transaction_modes(
3407    modes: &[TransactionMode],
3408    ctx: &RenderCtx<'_>,
3409    f: &mut fmt::Formatter<'_>,
3410) -> fmt::Result {
3411    render_leading_space_comma_separated(modes, ctx, f)
3412}
3413
3414impl Render for TransactionModeKind {
3415    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3416        f.write_str(match self {
3417            Self::Deferred => "DEFERRED",
3418            Self::Immediate => "IMMEDIATE",
3419            Self::Exclusive => "EXCLUSIVE",
3420        })
3421    }
3422}
3423
3424impl Render for TransactionMode {
3425    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3426        match self {
3427            Self::IsolationLevel { level, .. } => {
3428                f.write_str("ISOLATION LEVEL ")?;
3429                level.render(ctx, f)
3430            }
3431            Self::AccessMode { access, .. } => access.render(ctx, f),
3432            Self::Deferrable { deferrable, .. } => f.write_str(if *deferrable {
3433                "DEFERRABLE"
3434            } else {
3435                "NOT DEFERRABLE"
3436            }),
3437            Self::ConsistentSnapshot { .. } => f.write_str("WITH CONSISTENT SNAPSHOT"),
3438        }
3439    }
3440}
3441
3442impl Render for IsolationLevel {
3443    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3444        f.write_str(match self {
3445            Self::ReadUncommitted => "READ UNCOMMITTED",
3446            Self::ReadCommitted => "READ COMMITTED",
3447            Self::RepeatableRead => "REPEATABLE READ",
3448            Self::Serializable => "SERIALIZABLE",
3449        })
3450    }
3451}
3452
3453impl Render for TransactionAccessMode {
3454    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3455        f.write_str(match self {
3456            Self::ReadOnly => "READ ONLY",
3457            Self::ReadWrite => "READ WRITE",
3458        })
3459    }
3460}
3461
3462impl Render for XaStatement {
3463    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3464        match self {
3465            Self::Start {
3466                keyword,
3467                xid,
3468                association,
3469                ..
3470            } => {
3471                f.write_str(match keyword {
3472                    XaStartKeyword::Start => "XA START ",
3473                    XaStartKeyword::Begin => "XA BEGIN ",
3474                })?;
3475                xid.render(ctx, f)?;
3476                if let Some(association) = association {
3477                    f.write_str(match association {
3478                        XaAssociation::Join => " JOIN",
3479                        XaAssociation::Resume => " RESUME",
3480                    })?;
3481                }
3482                Ok(())
3483            }
3484            Self::End { xid, suspend, .. } => {
3485                f.write_str("XA END ")?;
3486                xid.render(ctx, f)?;
3487                if let Some(suspend) = suspend {
3488                    f.write_str(match suspend {
3489                        XaSuspend::Suspend => " SUSPEND",
3490                        XaSuspend::SuspendForMigrate => " SUSPEND FOR MIGRATE",
3491                    })?;
3492                }
3493                Ok(())
3494            }
3495            Self::Prepare { xid, .. } => {
3496                f.write_str("XA PREPARE ")?;
3497                xid.render(ctx, f)
3498            }
3499            Self::Commit { xid, one_phase, .. } => {
3500                f.write_str("XA COMMIT ")?;
3501                xid.render(ctx, f)?;
3502                if *one_phase {
3503                    f.write_str(" ONE PHASE")?;
3504                }
3505                Ok(())
3506            }
3507            Self::Rollback { xid, .. } => {
3508                f.write_str("XA ROLLBACK ")?;
3509                xid.render(ctx, f)
3510            }
3511            Self::Recover { convert_xid, .. } => {
3512                f.write_str("XA RECOVER")?;
3513                if *convert_xid {
3514                    f.write_str(" CONVERT XID")?;
3515                }
3516                Ok(())
3517            }
3518        }
3519    }
3520}
3521
3522impl Render for Xid {
3523    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3524        self.gtrid.render(ctx, f)?;
3525        if let Some(bqual) = &self.bqual {
3526            f.write_str(", ")?;
3527            bqual.render(ctx, f)?;
3528        }
3529        if let Some(format_id) = &self.format_id {
3530            f.write_str(", ")?;
3531            format_id.render(ctx, f)?;
3532        }
3533        Ok(())
3534    }
3535}
3536
3537impl<X: Extension + Render> Render for SessionStatement<X> {
3538    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3539        match self {
3540            Self::Set {
3541                scope,
3542                name,
3543                assignment,
3544                value,
3545                ..
3546            } => {
3547                f.write_str("SET")?;
3548                render_set_scope(*scope, ctx, f)?;
3549                f.write_str(" ")?;
3550                name.render(ctx, f)?;
3551                // `=` and `TO` are exact synonyms; a source-fidelity render replays the
3552                // written `=`, a target re-spell and the redacted fingerprint keep `TO`.
3553                f.write_str(match assignment {
3554                    SetAssignment::Equals if honours_source_spelling(ctx) => " = ",
3555                    _ => " TO ",
3556                })?;
3557                value.render(ctx, f)
3558            }
3559            Self::SetTimeZone { scope, value, .. } => {
3560                f.write_str("SET")?;
3561                render_set_scope(*scope, ctx, f)?;
3562                f.write_str(" TIME ZONE ")?;
3563                value.render(ctx, f)
3564            }
3565            Self::SetRole { scope, role, .. } => {
3566                f.write_str("SET")?;
3567                render_set_scope(*scope, ctx, f)?;
3568                f.write_str(" ROLE ")?;
3569                role.render(ctx, f)
3570            }
3571            Self::SetSessionAuthorization { scope, user, .. } => {
3572                f.write_str("SET")?;
3573                render_set_scope(*scope, ctx, f)?;
3574                f.write_str(" SESSION AUTHORIZATION ")?;
3575                user.render(ctx, f)
3576            }
3577            Self::SetConstraints {
3578                constraints,
3579                check_time,
3580                ..
3581            } => {
3582                f.write_str("SET CONSTRAINTS ")?;
3583                constraints.render(ctx, f)?;
3584                f.write_str(" ")?;
3585                check_time.render(ctx, f)
3586            }
3587            Self::SetNames { value, .. } => {
3588                f.write_str("SET NAMES ")?;
3589                value.render(ctx, f)
3590            }
3591            Self::SetSessionCharacteristics { modes, .. } => {
3592                f.write_str("SET SESSION CHARACTERISTICS AS TRANSACTION")?;
3593                render_transaction_modes(modes, ctx, f)
3594            }
3595            Self::Reset { scope, target, .. } => {
3596                f.write_str("RESET")?;
3597                render_set_scope(*scope, ctx, f)?;
3598                f.write_str(" ")?;
3599                target.render(ctx, f)
3600            }
3601            Self::Show {
3602                target, verbose, ..
3603            } => {
3604                f.write_str("SHOW ")?;
3605                target.render(ctx, f)?;
3606                if *verbose {
3607                    f.write_str(" VERBOSE")?;
3608                }
3609                Ok(())
3610            }
3611            Self::SetVariables { assignments, .. } => {
3612                f.write_str("SET ")?;
3613                render_comma_separated(assignments, ctx, f)
3614            }
3615            Self::SetCharacterSet { keyword, value, .. } => {
3616                f.write_str(match keyword {
3617                    CharacterSetKeyword::CharacterSet => "SET CHARACTER SET ",
3618                    CharacterSetKeyword::Charset => "SET CHARSET ",
3619                })?;
3620                value.render(ctx, f)
3621            }
3622            Self::SetResourceGroup {
3623                name, thread_ids, ..
3624            } => {
3625                f.write_str("SET RESOURCE GROUP ")?;
3626                name.render(ctx, f)?;
3627                if let Some(thread_ids) = thread_ids {
3628                    f.write_str(" FOR ")?;
3629                    render_comma_separated(thread_ids, ctx, f)?;
3630                }
3631                Ok(())
3632            }
3633        }
3634    }
3635}
3636
3637impl<X: Extension + Render> Render for SetVariableAssignment<X> {
3638    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3639        match self {
3640            Self::SystemVariable {
3641                scope,
3642                name,
3643                assignment,
3644                value,
3645                ..
3646            } => {
3647                scope.render_prefix(name, ctx, f)?;
3648                render_mysql_set_assignment(*assignment, ctx, f)?;
3649                value.render(ctx, f)
3650            }
3651            Self::UserVariable {
3652                name,
3653                assignment,
3654                value,
3655                ..
3656            } => {
3657                f.write_str("@")?;
3658                name.render(ctx, f)?;
3659                render_mysql_set_assignment(*assignment, ctx, f)?;
3660                value.render(ctx, f)
3661            }
3662        }
3663    }
3664}
3665
3666/// Render the `=` / `:=` separator of a MySQL variable assignment. The two are exact
3667/// synonyms; a source-fidelity render replays the written `:=`, a target re-spell and the
3668/// redacted fingerprint normalize to `=` (there is no `TO` spelling in MySQL's `SET`).
3669fn render_mysql_set_assignment(
3670    assignment: SetAssignment,
3671    ctx: &RenderCtx<'_>,
3672    f: &mut fmt::Formatter<'_>,
3673) -> fmt::Result {
3674    f.write_str(match assignment {
3675        SetAssignment::ColonEquals if honours_source_spelling(ctx) => " := ",
3676        _ => " = ",
3677    })
3678}
3679
3680impl SystemVariableScope {
3681    /// Render the scope prefix and the variable `name` together — a keyword prefix
3682    /// (`GLOBAL x`), the `@@` sigil (`@@x` / `@@global.x`), or the bare name.
3683    fn render_prefix(
3684        self,
3685        name: &ObjectName,
3686        ctx: &RenderCtx<'_>,
3687        f: &mut fmt::Formatter<'_>,
3688    ) -> fmt::Result {
3689        match self {
3690            Self::Implicit => name.render(ctx, f),
3691            Self::Keyword(kind) => {
3692                kind.render(ctx, f)?;
3693                f.write_str(" ")?;
3694                name.render(ctx, f)
3695            }
3696            Self::AtAt => {
3697                f.write_str("@@")?;
3698                name.render(ctx, f)
3699            }
3700            Self::AtAtScoped(kind) => {
3701                // The `@@scope.` prefix renders lowercase, matching the canonical
3702                // `Expr::SessionVariable` spelling (`@@global.` / `@@session.`).
3703                f.write_str("@@")?;
3704                f.write_str(kind.at_at_prefix())?;
3705                f.write_str(".")?;
3706                name.render(ctx, f)
3707            }
3708        }
3709    }
3710}
3711
3712impl SystemVariableScopeKind {
3713    /// The lowercase `@@`-form scope prefix word (`global`/`session`/…), matching the
3714    /// canonical `Expr::SessionVariable` spelling.
3715    fn at_at_prefix(self) -> &'static str {
3716        match self {
3717            Self::Global => "global",
3718            Self::Session => "session",
3719            Self::Local => "local",
3720            Self::Persist => "persist",
3721            Self::PersistOnly => "persist_only",
3722        }
3723    }
3724}
3725
3726impl Render for SystemVariableScopeKind {
3727    /// The uppercase keyword-prefix spelling (`SET GLOBAL x = …`).
3728    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3729        f.write_str(match self {
3730            Self::Global => "GLOBAL",
3731            Self::Session => "SESSION",
3732            Self::Local => "LOCAL",
3733            Self::Persist => "PERSIST",
3734            Self::PersistOnly => "PERSIST_ONLY",
3735        })
3736    }
3737}
3738
3739impl<X: Extension + Render> Render for SetVariableValue<X> {
3740    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3741        match self {
3742            Self::Default { .. } => f.write_str("DEFAULT"),
3743            Self::Keyword { keyword, .. } => keyword.render(ctx, f),
3744            Self::Expr { expr, .. } => expr.render(ctx, f),
3745        }
3746    }
3747}
3748
3749impl Render for SetVariableKeyword {
3750    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3751        f.write_str(match self {
3752            Self::On => "ON",
3753            Self::All => "ALL",
3754            Self::Binary => "BINARY",
3755            Self::Row => "ROW",
3756            Self::System => "SYSTEM",
3757        })
3758    }
3759}
3760
3761impl Render for SetCharacterSetValue {
3762    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3763        match self {
3764            Self::Default { .. } => f.write_str("DEFAULT"),
3765            Self::Charset { charset, .. } => charset.render(ctx, f),
3766        }
3767    }
3768}
3769
3770impl Render for AlterSystem {
3771    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3772        f.write_str("ALTER SYSTEM ")?;
3773        match &self.action {
3774            AlterSystemAction::Set {
3775                name,
3776                assignment,
3777                value,
3778                ..
3779            } => {
3780                f.write_str("SET ")?;
3781                name.render(ctx, f)?;
3782                // `=` and `TO` are exact synonyms; a source-fidelity render replays the
3783                // written `=`, a target re-spell and the redacted fingerprint keep `TO`.
3784                f.write_str(match assignment {
3785                    SetAssignment::Equals if honours_source_spelling(ctx) => " = ",
3786                    _ => " TO ",
3787                })?;
3788                value.render(ctx, f)
3789            }
3790            AlterSystemAction::Reset { target, .. } => {
3791                f.write_str("RESET ")?;
3792                target.render(ctx, f)
3793            }
3794        }
3795    }
3796}
3797
3798impl Render for AlterDatabase {
3799    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3800        f.write_str("ALTER DATABASE ")?;
3801        if self.if_exists {
3802            f.write_str("IF EXISTS ")?;
3803        }
3804        self.name.render(ctx, f)?;
3805        match &self.action {
3806            AlterDatabaseAction::SetAlias { new_name, .. } => {
3807                f.write_str(" SET ALIAS TO ")?;
3808                new_name.render(ctx, f)
3809            }
3810        }
3811    }
3812}
3813
3814impl Render for AlterDatabaseOptions {
3815    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3816        f.write_str(match self.spelling {
3817            DatabaseKeyword::Database => "ALTER DATABASE",
3818            DatabaseKeyword::Schema => "ALTER SCHEMA",
3819        })?;
3820        if let Some(name) = &self.name {
3821            f.write_str(" ")?;
3822            name.render(ctx, f)?;
3823        }
3824        for option in &self.options {
3825            f.write_str(" ")?;
3826            option.render(ctx, f)?;
3827        }
3828        Ok(())
3829    }
3830}
3831
3832impl Render for AlterDatabaseOption {
3833    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3834        // `[=]` presence round-trips verbatim (the `IndexLockAlgorithmOption` precedent), so the
3835        // written spacing is reproduced in both source-fidelity and canonical renders.
3836        let sep = |equals: bool| if equals { " = " } else { " " };
3837        match self {
3838            Self::CharacterSet {
3839                default,
3840                keyword,
3841                equals,
3842                charset,
3843                ..
3844            } => {
3845                if *default {
3846                    f.write_str("DEFAULT ")?;
3847                }
3848                f.write_str(match keyword {
3849                    CharsetKeyword::CharacterSet => "CHARACTER SET",
3850                    CharsetKeyword::Charset => "CHARSET",
3851                })?;
3852                f.write_str(sep(*equals))?;
3853                charset.render(ctx, f)
3854            }
3855            Self::Collate {
3856                default,
3857                equals,
3858                collation,
3859                ..
3860            } => {
3861                if *default {
3862                    f.write_str("DEFAULT ")?;
3863                }
3864                f.write_str("COLLATE")?;
3865                f.write_str(sep(*equals))?;
3866                collation.render(ctx, f)
3867            }
3868            Self::Encryption {
3869                default,
3870                equals,
3871                value,
3872                ..
3873            } => {
3874                if *default {
3875                    f.write_str("DEFAULT ")?;
3876                }
3877                f.write_str("ENCRYPTION")?;
3878                f.write_str(sep(*equals))?;
3879                value.render(ctx, f)
3880            }
3881            Self::ReadOnly { equals, value, .. } => {
3882                f.write_str("READ ONLY")?;
3883                f.write_str(sep(*equals))?;
3884                f.write_str(match value {
3885                    ReadOnlyValue::Default => "DEFAULT",
3886                    ReadOnlyValue::Off => "0",
3887                    ReadOnlyValue::On => "1",
3888                })
3889            }
3890        }
3891    }
3892}
3893
3894impl Render for CreateServer {
3895    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3896        f.write_str("CREATE SERVER ")?;
3897        self.name.render(ctx, f)?;
3898        f.write_str(" FOREIGN DATA WRAPPER ")?;
3899        self.wrapper.render(ctx, f)?;
3900        f.write_str(" OPTIONS (")?;
3901        render_server_options(&self.options, ctx, f)?;
3902        f.write_str(")")
3903    }
3904}
3905
3906impl Render for AlterServer {
3907    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3908        f.write_str("ALTER SERVER ")?;
3909        self.name.render(ctx, f)?;
3910        f.write_str(" OPTIONS (")?;
3911        render_server_options(&self.options, ctx, f)?;
3912        f.write_str(")")
3913    }
3914}
3915
3916impl Render for DropServer {
3917    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3918        f.write_str("DROP SERVER ")?;
3919        if self.if_exists {
3920            f.write_str("IF EXISTS ")?;
3921        }
3922        self.name.render(ctx, f)
3923    }
3924}
3925
3926/// The shared `<option>[, ...]` body of a `CREATE`/`ALTER SERVER` `OPTIONS ( … )` list
3927/// (comma-separated, no surrounding parentheses — the caller writes those).
3928fn render_server_options(
3929    options: &[ServerOption],
3930    ctx: &RenderCtx<'_>,
3931    f: &mut fmt::Formatter<'_>,
3932) -> fmt::Result {
3933    for (i, option) in options.iter().enumerate() {
3934        if i > 0 {
3935            f.write_str(", ")?;
3936        }
3937        option.render(ctx, f)?;
3938    }
3939    Ok(())
3940}
3941
3942impl Render for ServerOption {
3943    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3944        f.write_str(match self.kind {
3945            ServerOptionKind::Host => "HOST ",
3946            ServerOptionKind::Database => "DATABASE ",
3947            ServerOptionKind::User => "USER ",
3948            ServerOptionKind::Password => "PASSWORD ",
3949            ServerOptionKind::Socket => "SOCKET ",
3950            ServerOptionKind::Owner => "OWNER ",
3951            ServerOptionKind::Port => "PORT ",
3952        })?;
3953        self.value.render(ctx, f)
3954    }
3955}
3956
3957impl Render for CreateSpatialReferenceSystem {
3958    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3959        f.write_str("CREATE ")?;
3960        if self.or_replace {
3961            f.write_str("OR REPLACE ")?;
3962        }
3963        f.write_str("SPATIAL REFERENCE SYSTEM ")?;
3964        if self.if_not_exists {
3965            f.write_str("IF NOT EXISTS ")?;
3966        }
3967        self.srid.render(ctx, f)?;
3968        for attribute in &self.attributes {
3969            f.write_str(" ")?;
3970            attribute.render(ctx, f)?;
3971        }
3972        Ok(())
3973    }
3974}
3975
3976impl Render for SrsAttribute {
3977    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3978        match self {
3979            Self::Name { value, .. } => {
3980                f.write_str("NAME ")?;
3981                value.render(ctx, f)
3982            }
3983            Self::Definition { value, .. } => {
3984                f.write_str("DEFINITION ")?;
3985                value.render(ctx, f)
3986            }
3987            Self::Organization {
3988                organization,
3989                identifier,
3990                ..
3991            } => {
3992                f.write_str("ORGANIZATION ")?;
3993                organization.render(ctx, f)?;
3994                f.write_str(" IDENTIFIED BY ")?;
3995                identifier.render(ctx, f)
3996            }
3997            Self::Description { value, .. } => {
3998                f.write_str("DESCRIPTION ")?;
3999                value.render(ctx, f)
4000            }
4001        }
4002    }
4003}
4004
4005impl Render for DropSpatialReferenceSystem {
4006    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4007        f.write_str("DROP SPATIAL REFERENCE SYSTEM ")?;
4008        if self.if_exists {
4009            f.write_str("IF EXISTS ")?;
4010        }
4011        self.srid.render(ctx, f)
4012    }
4013}
4014
4015impl Render for CreateResourceGroup {
4016    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4017        f.write_str("CREATE RESOURCE GROUP ")?;
4018        self.name.render(ctx, f)?;
4019        f.write_str(if self.type_equals {
4020            " TYPE = "
4021        } else {
4022            " TYPE "
4023        })?;
4024        self.group_type.render(ctx, f)?;
4025        if let Some(vcpu) = &self.vcpu {
4026            f.write_str(" ")?;
4027            vcpu.render(ctx, f)?;
4028        }
4029        if let Some(priority) = &self.thread_priority {
4030            f.write_str(" ")?;
4031            priority.render(ctx, f)?;
4032        }
4033        if let Some(state) = &self.state {
4034            f.write_str(" ")?;
4035            state.render(ctx, f)?;
4036        }
4037        Ok(())
4038    }
4039}
4040
4041impl Render for AlterResourceGroup {
4042    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4043        f.write_str("ALTER RESOURCE GROUP ")?;
4044        self.name.render(ctx, f)?;
4045        if let Some(vcpu) = &self.vcpu {
4046            f.write_str(" ")?;
4047            vcpu.render(ctx, f)?;
4048        }
4049        if let Some(priority) = &self.thread_priority {
4050            f.write_str(" ")?;
4051            priority.render(ctx, f)?;
4052        }
4053        if let Some(state) = &self.state {
4054            f.write_str(" ")?;
4055            state.render(ctx, f)?;
4056        }
4057        if self.force {
4058            f.write_str(" FORCE")?;
4059        }
4060        Ok(())
4061    }
4062}
4063
4064impl Render for DropResourceGroup {
4065    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4066        f.write_str("DROP RESOURCE GROUP ")?;
4067        self.name.render(ctx, f)?;
4068        if self.force {
4069            f.write_str(" FORCE")?;
4070        }
4071        Ok(())
4072    }
4073}
4074
4075impl Render for ResourceGroupType {
4076    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4077        f.write_str(match self {
4078            Self::System => "SYSTEM",
4079            Self::User => "USER",
4080        })
4081    }
4082}
4083
4084impl Render for ResourceGroupState {
4085    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4086        f.write_str(match self {
4087            Self::Enable => "ENABLE",
4088            Self::Disable => "DISABLE",
4089        })
4090    }
4091}
4092
4093impl Render for ResourceGroupVcpu {
4094    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4095        f.write_str(if self.equals { "VCPU = " } else { "VCPU " })?;
4096        for (i, range) in self.ranges.iter().enumerate() {
4097            if i > 0 {
4098                f.write_str(", ")?;
4099            }
4100            range.render(ctx, f)?;
4101        }
4102        Ok(())
4103    }
4104}
4105
4106impl Render for VcpuRange {
4107    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4108        self.start.render(ctx, f)?;
4109        if let Some(end) = &self.end {
4110            f.write_str("-")?;
4111            end.render(ctx, f)?;
4112        }
4113        Ok(())
4114    }
4115}
4116
4117impl Render for ResourceGroupThreadPriority {
4118    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4119        f.write_str(if self.equals {
4120            "THREAD_PRIORITY = "
4121        } else {
4122            "THREAD_PRIORITY "
4123        })?;
4124        if self.negative {
4125            f.write_str("-")?;
4126        }
4127        self.value.render(ctx, f)
4128    }
4129}
4130
4131impl Render for AlterInstance {
4132    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4133        f.write_str("ALTER INSTANCE ")?;
4134        self.action.render(ctx, f)
4135    }
4136}
4137
4138impl Render for AlterInstanceAction {
4139    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4140        match self {
4141            Self::RotateInnodbMasterKey { .. } => f.write_str("ROTATE INNODB MASTER KEY"),
4142            Self::RotateBinlogMasterKey { .. } => f.write_str("ROTATE BINLOG MASTER KEY"),
4143            Self::ReloadTls {
4144                channel,
4145                no_rollback_on_error,
4146                ..
4147            } => {
4148                f.write_str("RELOAD TLS")?;
4149                if let Some(channel) = channel {
4150                    f.write_str(" FOR CHANNEL ")?;
4151                    channel.render(ctx, f)?;
4152                }
4153                if *no_rollback_on_error {
4154                    f.write_str(" NO ROLLBACK ON ERROR")?;
4155                }
4156                Ok(())
4157            }
4158            Self::ReloadKeyring { .. } => f.write_str("RELOAD KEYRING"),
4159            Self::EnableInnodbRedoLog { .. } => f.write_str("ENABLE INNODB REDO_LOG"),
4160            Self::DisableInnodbRedoLog { .. } => f.write_str("DISABLE INNODB REDO_LOG"),
4161        }
4162    }
4163}
4164
4165impl<X: Extension + Render> Render for AlterSequence<X> {
4166    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4167        f.write_str("ALTER SEQUENCE ")?;
4168        if self.if_exists {
4169            f.write_str("IF EXISTS ")?;
4170        }
4171        self.name.render(ctx, f)?;
4172        // Options are space-separated in their canonical spelling; the shared core reuses the
4173        // `IdentityOption` render.
4174        for option in &self.options {
4175            f.write_str(" ")?;
4176            option.render(ctx, f)?;
4177        }
4178        Ok(())
4179    }
4180}
4181
4182impl<X: Extension + Render> Render for AlterSequenceOption<X> {
4183    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4184        match self {
4185            Self::Common { option, .. } => option.render(ctx, f),
4186            Self::Restart { value: None, .. } => f.write_str("RESTART"),
4187            Self::Restart {
4188                value: Some(expr), ..
4189            } => {
4190                f.write_str("RESTART WITH ")?;
4191                expr.render(ctx, f)
4192            }
4193            Self::As { data_type, .. } => {
4194                f.write_str("AS ")?;
4195                data_type.render(ctx, f)
4196            }
4197            Self::OwnedBy { owner: None, .. } => f.write_str("OWNED BY NONE"),
4198            Self::OwnedBy {
4199                owner: Some(name), ..
4200            } => {
4201                f.write_str("OWNED BY ")?;
4202                name.render(ctx, f)
4203            }
4204            Self::SequenceName { name, .. } => {
4205                f.write_str("SEQUENCE NAME ")?;
4206                name.render(ctx, f)
4207            }
4208        }
4209    }
4210}
4211
4212impl Render for AlterObjectSchema {
4213    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4214        f.write_str("ALTER ")?;
4215        f.write_str(match self.object_type {
4216            SchemaRelocationObject::Table => "TABLE ",
4217            SchemaRelocationObject::View => "VIEW ",
4218            SchemaRelocationObject::Sequence => "SEQUENCE ",
4219        })?;
4220        if self.if_exists {
4221            f.write_str("IF EXISTS ")?;
4222        }
4223        self.name.render(ctx, f)?;
4224        f.write_str(" SET SCHEMA ")?;
4225        self.new_schema.render(ctx, f)
4226    }
4227}
4228
4229/// Render an optional `SET` scope as ` SESSION` / ` LOCAL` (leading space), or
4230/// nothing when absent — shared by the generic and special `SET` forms.
4231fn render_set_scope(
4232    scope: Option<SetScope>,
4233    ctx: &RenderCtx<'_>,
4234    f: &mut fmt::Formatter<'_>,
4235) -> fmt::Result {
4236    if let Some(scope) = scope {
4237        f.write_str(" ")?;
4238        scope.render(ctx, f)?;
4239    }
4240    Ok(())
4241}
4242
4243impl Render for SpecialSetValue {
4244    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4245        match self {
4246            Self::Default { .. } => f.write_str("DEFAULT"),
4247            Self::Local { .. } => f.write_str("LOCAL"),
4248            Self::None { .. } => f.write_str("NONE"),
4249            Self::Value { value, .. } => value.render(ctx, f),
4250        }
4251    }
4252}
4253
4254impl Render for ConstraintsTarget {
4255    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4256        match self {
4257            Self::All { .. } => f.write_str("ALL"),
4258            Self::Names { names, .. } => render_comma_separated(names, ctx, f),
4259        }
4260    }
4261}
4262
4263impl Render for ConstraintCheckTime {
4264    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4265        f.write_str(match self {
4266            Self::Deferred => "DEFERRED",
4267            Self::Immediate => "IMMEDIATE",
4268        })
4269    }
4270}
4271
4272impl Render for SetNamesValue {
4273    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4274        match self {
4275            Self::Default { .. } => f.write_str("DEFAULT"),
4276            Self::Charset {
4277                charset, collation, ..
4278            } => {
4279                charset.render(ctx, f)?;
4280                if let Some(collation) = collation {
4281                    f.write_str(" COLLATE ")?;
4282                    collation.render(ctx, f)?;
4283                }
4284                Ok(())
4285            }
4286        }
4287    }
4288}
4289
4290impl Render for SetScope {
4291    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4292        f.write_str(match self {
4293            Self::Session => "SESSION",
4294            Self::Local => "LOCAL",
4295            Self::Global => "GLOBAL",
4296        })
4297    }
4298}
4299
4300impl Render for SetValue {
4301    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4302        match self {
4303            Self::Default { .. } => f.write_str("DEFAULT"),
4304            Self::Values { values, .. } => render_comma_separated(values, ctx, f),
4305        }
4306    }
4307}
4308
4309impl Render for SetParameterValue {
4310    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4311        match self {
4312            Self::Literal { literal, .. } => literal.render(ctx, f),
4313            Self::Name { name, .. } => name.render(ctx, f),
4314            Self::Parameter { kind, .. } => render_parameter_kind(*kind, ctx, f),
4315            Self::List { values, .. } => {
4316                f.write_str("[")?;
4317                render_comma_separated(values, ctx, f)?;
4318                f.write_str("]")
4319            }
4320        }
4321    }
4322}
4323
4324fn render_parameter_kind(
4325    kind: ParameterKind,
4326    ctx: &RenderCtx<'_>,
4327    f: &mut fmt::Formatter<'_>,
4328) -> fmt::Result {
4329    match kind {
4330        ParameterKind::Positional(index) => write!(f, "${index}"),
4331        ParameterKind::PositionalLarge { digits } => write!(f, "${}", ctx.resolve(digits)),
4332        ParameterKind::Numbered(index) => write!(f, "?{index}"),
4333        ParameterKind::Anonymous => f.write_str("?"),
4334        ParameterKind::Named { name, sigil } => {
4335            let sigil = match sigil {
4336                ParameterSigil::Colon => ':',
4337                ParameterSigil::At => '@',
4338                ParameterSigil::Dollar => '$',
4339            };
4340            write!(f, "{sigil}{}", ctx.resolve(name))
4341        }
4342    }
4343}
4344
4345impl Render for ConfigParameter {
4346    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4347        match self {
4348            Self::All { .. } => f.write_str("ALL"),
4349            Self::Named { name, .. } => name.render(ctx, f),
4350        }
4351    }
4352}
4353
4354impl<X: Extension + Render> Render for AccessControlStatement<X> {
4355    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4356        match self {
4357            Self::AlterRoleRename { name, new_name, .. } => {
4358                f.write_str("ALTER ROLE ")?;
4359                name.render(ctx, f)?;
4360                f.write_str(" RENAME TO ")?;
4361                new_name.render(ctx, f)
4362            }
4363            Self::Grant {
4364                privileges,
4365                object,
4366                grantees,
4367                with_grant_option,
4368                granted_by,
4369                ..
4370            } => {
4371                f.write_str("GRANT ")?;
4372                privileges.render(ctx, f)?;
4373                f.write_str(" ON ")?;
4374                object.render(ctx, f)?;
4375                f.write_str(" TO ")?;
4376                render_comma_separated(grantees, ctx, f)?;
4377                if *with_grant_option {
4378                    f.write_str(" WITH GRANT OPTION")?;
4379                }
4380                render_granted_by(granted_by, ctx, f)
4381            }
4382            Self::Revoke {
4383                grant_option_for,
4384                privileges,
4385                object,
4386                grantees,
4387                granted_by,
4388                behavior,
4389                ..
4390            } => {
4391                f.write_str("REVOKE ")?;
4392                if *grant_option_for {
4393                    f.write_str("GRANT OPTION FOR ")?;
4394                }
4395                privileges.render(ctx, f)?;
4396                f.write_str(" ON ")?;
4397                object.render(ctx, f)?;
4398                f.write_str(" FROM ")?;
4399                render_comma_separated(grantees, ctx, f)?;
4400                render_granted_by(granted_by, ctx, f)?;
4401                render_drop_behavior(*behavior, ctx, f)
4402            }
4403            Self::GrantRole {
4404                roles,
4405                grantees,
4406                with_admin_option,
4407                granted_by,
4408                ..
4409            } => {
4410                f.write_str("GRANT ")?;
4411                render_ident_list(roles, ctx, f)?;
4412                f.write_str(" TO ")?;
4413                render_comma_separated(grantees, ctx, f)?;
4414                if *with_admin_option {
4415                    f.write_str(" WITH ADMIN OPTION")?;
4416                }
4417                render_granted_by(granted_by, ctx, f)
4418            }
4419            Self::RevokeRole {
4420                admin_option_for,
4421                roles,
4422                grantees,
4423                granted_by,
4424                behavior,
4425                ..
4426            } => {
4427                f.write_str("REVOKE ")?;
4428                if *admin_option_for {
4429                    f.write_str("ADMIN OPTION FOR ")?;
4430                }
4431                render_ident_list(roles, ctx, f)?;
4432                f.write_str(" FROM ")?;
4433                render_comma_separated(grantees, ctx, f)?;
4434                render_granted_by(granted_by, ctx, f)?;
4435                render_drop_behavior(*behavior, ctx, f)
4436            }
4437            Self::AccountGrantPrivilege {
4438                privileges,
4439                object,
4440                grantees,
4441                with_grant_option,
4442                grant_as,
4443                ..
4444            } => {
4445                f.write_str("GRANT ")?;
4446                privileges.render(ctx, f)?;
4447                f.write_str(" ON ")?;
4448                object.render(ctx, f)?;
4449                f.write_str(" TO ")?;
4450                render_comma_separated(grantees, ctx, f)?;
4451                if *with_grant_option {
4452                    f.write_str(" WITH GRANT OPTION")?;
4453                }
4454                if let Some(grant_as) = grant_as {
4455                    f.write_str(" ")?;
4456                    grant_as.render(ctx, f)?;
4457                }
4458                Ok(())
4459            }
4460            Self::AccountGrantProxy {
4461                proxy,
4462                grantees,
4463                with_grant_option,
4464                ..
4465            } => {
4466                f.write_str("GRANT PROXY ON ")?;
4467                proxy.render(ctx, f)?;
4468                f.write_str(" TO ")?;
4469                render_comma_separated(grantees, ctx, f)?;
4470                if *with_grant_option {
4471                    f.write_str(" WITH GRANT OPTION")?;
4472                }
4473                Ok(())
4474            }
4475            Self::AccountGrantRole {
4476                roles,
4477                grantees,
4478                with_admin_option,
4479                ..
4480            } => {
4481                f.write_str("GRANT ")?;
4482                render_comma_separated(roles, ctx, f)?;
4483                f.write_str(" TO ")?;
4484                render_comma_separated(grantees, ctx, f)?;
4485                if *with_admin_option {
4486                    f.write_str(" WITH ADMIN OPTION")?;
4487                }
4488                Ok(())
4489            }
4490            Self::AccountRevokePrivilege {
4491                if_exists,
4492                privileges,
4493                object,
4494                grantees,
4495                ignore_unknown_user,
4496                ..
4497            } => {
4498                f.write_str("REVOKE ")?;
4499                if *if_exists {
4500                    f.write_str("IF EXISTS ")?;
4501                }
4502                privileges.render(ctx, f)?;
4503                f.write_str(" ON ")?;
4504                object.render(ctx, f)?;
4505                f.write_str(" FROM ")?;
4506                render_comma_separated(grantees, ctx, f)?;
4507                render_ignore_unknown_user(*ignore_unknown_user, f)
4508            }
4509            Self::AccountRevokeAll {
4510                if_exists,
4511                privileges_keyword,
4512                grantees,
4513                ignore_unknown_user,
4514                ..
4515            } => {
4516                f.write_str("REVOKE ")?;
4517                if *if_exists {
4518                    f.write_str("IF EXISTS ")?;
4519                }
4520                // The optional `PRIVILEGES` noise word is fidelity-only; the canonical render
4521                // emits it.
4522                if *privileges_keyword || !honours_source_spelling(ctx) {
4523                    f.write_str("ALL PRIVILEGES, GRANT OPTION FROM ")?;
4524                } else {
4525                    f.write_str("ALL, GRANT OPTION FROM ")?;
4526                }
4527                render_comma_separated(grantees, ctx, f)?;
4528                render_ignore_unknown_user(*ignore_unknown_user, f)
4529            }
4530            Self::AccountRevokeProxy {
4531                if_exists,
4532                proxy,
4533                grantees,
4534                ignore_unknown_user,
4535                ..
4536            } => {
4537                f.write_str("REVOKE ")?;
4538                if *if_exists {
4539                    f.write_str("IF EXISTS ")?;
4540                }
4541                f.write_str("PROXY ON ")?;
4542                proxy.render(ctx, f)?;
4543                f.write_str(" FROM ")?;
4544                render_comma_separated(grantees, ctx, f)?;
4545                render_ignore_unknown_user(*ignore_unknown_user, f)
4546            }
4547            Self::AccountRevokeRole {
4548                if_exists,
4549                roles,
4550                grantees,
4551                ignore_unknown_user,
4552                ..
4553            } => {
4554                f.write_str("REVOKE ")?;
4555                if *if_exists {
4556                    f.write_str("IF EXISTS ")?;
4557                }
4558                render_comma_separated(roles, ctx, f)?;
4559                f.write_str(" FROM ")?;
4560                render_comma_separated(grantees, ctx, f)?;
4561                render_ignore_unknown_user(*ignore_unknown_user, f)
4562            }
4563        }
4564    }
4565}
4566
4567/// Render the MySQL `IGNORE UNKNOWN USER` trailer, when present.
4568fn render_ignore_unknown_user(
4569    ignore_unknown_user: bool,
4570    f: &mut fmt::Formatter<'_>,
4571) -> fmt::Result {
4572    if ignore_unknown_user {
4573        f.write_str(" IGNORE UNKNOWN USER")?;
4574    }
4575    Ok(())
4576}
4577
4578impl Render for PrivilegeLevelObject {
4579    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4580        match self.object_type {
4581            PrivilegeObjectType::Table { explicit } => {
4582                if explicit {
4583                    f.write_str("TABLE ")?;
4584                }
4585            }
4586            PrivilegeObjectType::Function => f.write_str("FUNCTION ")?,
4587            PrivilegeObjectType::Procedure => f.write_str("PROCEDURE ")?,
4588        }
4589        self.level.render(ctx, f)
4590    }
4591}
4592
4593impl Render for PrivilegeLevel {
4594    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4595        match self {
4596            Self::Global { .. } => f.write_str("*.*"),
4597            Self::CurrentDatabase { .. } => f.write_str("*"),
4598            Self::Database { database, .. } => {
4599                database.render(ctx, f)?;
4600                f.write_str(".*")
4601            }
4602            Self::Object { name, .. } => name.render(ctx, f),
4603        }
4604    }
4605}
4606
4607impl Render for GrantAs {
4608    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4609        f.write_str("AS ")?;
4610        self.user.render(ctx, f)?;
4611        if let Some(with_role) = &self.with_role {
4612            f.write_str(" WITH ROLE ")?;
4613            with_role.render(ctx, f)?;
4614        }
4615        Ok(())
4616    }
4617}
4618
4619impl Render for WithRoleSpec {
4620    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4621        match self {
4622            Self::Roles { roles, .. } => render_comma_separated(roles, ctx, f),
4623            Self::All { except, .. } => {
4624                f.write_str("ALL")?;
4625                if !except.is_empty() {
4626                    f.write_str(" EXCEPT ")?;
4627                    render_comma_separated(except, ctx, f)?;
4628                }
4629                Ok(())
4630            }
4631            Self::None { .. } => f.write_str("NONE"),
4632            Self::Default { .. } => f.write_str("DEFAULT"),
4633        }
4634    }
4635}
4636
4637/// Render an optional `GRANTED BY <grantor>` trailer.
4638fn render_granted_by(
4639    granted_by: &Option<RoleSpec>,
4640    ctx: &RenderCtx<'_>,
4641    f: &mut fmt::Formatter<'_>,
4642) -> fmt::Result {
4643    if let Some(grantor) = granted_by {
4644        f.write_str(" GRANTED BY ")?;
4645        grantor.render(ctx, f)?;
4646    }
4647    Ok(())
4648}
4649
4650impl Render for Privileges {
4651    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4652        match self {
4653            // The optional `PRIVILEGES` keyword is exact-synonym sugar: the canonical
4654            // render emits it, a source-fidelity render drops it when the source did.
4655            Self::All {
4656                privileges_keyword, ..
4657            } => {
4658                if *privileges_keyword || !honours_source_spelling(ctx) {
4659                    f.write_str("ALL PRIVILEGES")
4660                } else {
4661                    f.write_str("ALL")
4662                }
4663            }
4664            Self::List { privileges, .. } => render_comma_separated(privileges, ctx, f),
4665        }
4666    }
4667}
4668
4669impl Render for Privilege {
4670    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4671        match self {
4672            Self::Known { kind, columns, .. } => {
4673                kind.render(ctx, f)?;
4674                render_privilege_columns(columns, ctx, f)
4675            }
4676            Self::Other { name, columns, .. } => {
4677                name.render(ctx, f)?;
4678                render_privilege_columns(columns, ctx, f)
4679            }
4680        }
4681    }
4682}
4683
4684/// Render a privilege's optional `( <column> [, ...] )` scope.
4685fn render_privilege_columns(
4686    columns: &[Ident],
4687    ctx: &RenderCtx<'_>,
4688    f: &mut fmt::Formatter<'_>,
4689) -> fmt::Result {
4690    if !columns.is_empty() {
4691        f.write_str(" (")?;
4692        render_ident_list(columns, ctx, f)?;
4693        f.write_str(")")?;
4694    }
4695    Ok(())
4696}
4697
4698impl Render for PrivilegeKind {
4699    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4700        f.write_str(match self {
4701            Self::Select => "SELECT",
4702            Self::Insert => "INSERT",
4703            Self::Update => "UPDATE",
4704            Self::Delete => "DELETE",
4705            Self::Truncate => "TRUNCATE",
4706            Self::References => "REFERENCES",
4707            Self::Trigger => "TRIGGER",
4708            Self::Usage => "USAGE",
4709            Self::Execute => "EXECUTE",
4710            Self::Create => "CREATE",
4711            Self::Connect => "CONNECT",
4712            Self::Temporary => "TEMPORARY",
4713            Self::Temp => "TEMP",
4714            Self::Maintain => "MAINTAIN",
4715            Self::Index => "INDEX",
4716            Self::Alter => "ALTER",
4717            Self::Drop => "DROP",
4718            Self::Reload => "RELOAD",
4719            Self::Shutdown => "SHUTDOWN",
4720            Self::Process => "PROCESS",
4721            Self::File => "FILE",
4722            Self::Super => "SUPER",
4723            Self::Event => "EVENT",
4724            Self::GrantOption => "GRANT OPTION",
4725            Self::ShowDatabases => "SHOW DATABASES",
4726            Self::CreateTemporaryTables => "CREATE TEMPORARY TABLES",
4727            Self::LockTables => "LOCK TABLES",
4728            Self::ReplicationSlave => "REPLICATION SLAVE",
4729            Self::ReplicationClient => "REPLICATION CLIENT",
4730            Self::CreateView => "CREATE VIEW",
4731            Self::ShowView => "SHOW VIEW",
4732            Self::CreateRoutine => "CREATE ROUTINE",
4733            Self::AlterRoutine => "ALTER ROUTINE",
4734            Self::CreateUser => "CREATE USER",
4735            Self::CreateTablespace => "CREATE TABLESPACE",
4736            Self::CreateRole => "CREATE ROLE",
4737            Self::DropRole => "DROP ROLE",
4738        })
4739    }
4740}
4741
4742impl<X: Extension + Render> Render for GrantObject<X> {
4743    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4744        match self {
4745            Self::Table {
4746                explicit, names, ..
4747            } => {
4748                if *explicit {
4749                    f.write_str("TABLE ")?;
4750                }
4751                render_comma_separated(names, ctx, f)
4752            }
4753            Self::Named { kind, names, .. } => {
4754                kind.render(ctx, f)?;
4755                f.write_str(" ")?;
4756                render_comma_separated(names, ctx, f)
4757            }
4758            Self::Routines { kind, routines, .. } => {
4759                kind.render(ctx, f)?;
4760                f.write_str(" ")?;
4761                render_comma_separated(routines, ctx, f)
4762            }
4763            Self::AllInSchema { kind, schemas, .. } => {
4764                f.write_str("ALL ")?;
4765                kind.render(ctx, f)?;
4766                f.write_str(" IN SCHEMA ")?;
4767                render_comma_separated(schemas, ctx, f)
4768            }
4769        }
4770    }
4771}
4772
4773impl Render for NamedObjectKind {
4774    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4775        f.write_str(match self {
4776            Self::Sequence => "SEQUENCE",
4777            Self::Database => "DATABASE",
4778            Self::Schema => "SCHEMA",
4779            Self::Domain => "DOMAIN",
4780            Self::Type => "TYPE",
4781            Self::Language => "LANGUAGE",
4782            Self::Tablespace => "TABLESPACE",
4783            Self::ForeignDataWrapper => "FOREIGN DATA WRAPPER",
4784            Self::ForeignServer => "FOREIGN SERVER",
4785        })
4786    }
4787}
4788
4789impl Render for RoutineObjectKind {
4790    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4791        f.write_str(match self {
4792            Self::Function => "FUNCTION",
4793            Self::Procedure => "PROCEDURE",
4794            Self::Routine => "ROUTINE",
4795        })
4796    }
4797}
4798
4799impl Render for SchemaObjectKind {
4800    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4801        f.write_str(match self {
4802            Self::Tables => "TABLES",
4803            Self::Sequences => "SEQUENCES",
4804            Self::Functions => "FUNCTIONS",
4805            Self::Procedures => "PROCEDURES",
4806            Self::Routines => "ROUTINES",
4807        })
4808    }
4809}
4810
4811impl<X: Extension + Render> Render for RoutineSignature<X> {
4812    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4813        self.name.render(ctx, f)?;
4814        if let Some(arg_types) = &self.arg_types {
4815            f.write_str("(")?;
4816            render_comma_separated(arg_types, ctx, f)?;
4817            f.write_str(")")?;
4818        }
4819        Ok(())
4820    }
4821}
4822
4823impl Render for Grantee {
4824    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4825        if self.group {
4826            f.write_str("GROUP ")?;
4827        }
4828        self.spec.render(ctx, f)
4829    }
4830}
4831
4832impl Render for RoleSpec {
4833    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4834        match self {
4835            Self::Public { .. } => f.write_str("PUBLIC"),
4836            Self::CurrentRole { .. } => f.write_str("CURRENT_ROLE"),
4837            Self::CurrentUser { .. } => f.write_str("CURRENT_USER"),
4838            Self::SessionUser { .. } => f.write_str("SESSION_USER"),
4839            Self::Name { name, .. } => name.render(ctx, f),
4840        }
4841    }
4842}
4843
4844impl<X: Extension + Render> Render for CopyStatement<X> {
4845    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4846        f.write_str("COPY ")?;
4847        // The `opt_binary` prefix, when written.
4848        if self.binary {
4849            f.write_str("BINARY ")?;
4850        }
4851        self.source.render(ctx, f)?;
4852        f.write_str(" ")?;
4853        self.direction.render(ctx, f)?;
4854        f.write_str(" ")?;
4855        self.target.render(ctx, f)?;
4856        // The `[USING] DELIMITERS '<str>'` clause, rendered in canonical form
4857        // (the optional `USING` is dropped) between the endpoint and the options.
4858        if let Some(delimiters) = &self.delimiters {
4859            f.write_str(" DELIMITERS ")?;
4860            delimiters.render(ctx, f)?;
4861        }
4862        // An empty option list renders nothing in either spelling, so the surface
4863        // tag only matters when populated. The parenthesized spelling canonicalizes
4864        // the optional `WITH` as present; the legacy spelling is space-separated
4865        // with neither `WITH` nor parentheses.
4866        if !self.options.is_empty() {
4867            if self.parenthesized {
4868                f.write_str(" WITH (")?;
4869                render_comma_separated(&self.options, ctx, f)?;
4870                f.write_str(")")?;
4871            } else {
4872                for option in &self.options {
4873                    f.write_str(" ")?;
4874                    option.render(ctx, f)?;
4875                }
4876            }
4877        }
4878        // The `COPY FROM ... WHERE <predicate>` filter, last (the parser only ever
4879        // populates it on a `FROM` table source).
4880        if let Some(filter) = &self.filter {
4881            f.write_str(" WHERE ")?;
4882            filter.render(ctx, f)?;
4883        }
4884        Ok(())
4885    }
4886}
4887
4888impl<X: Extension + Render> Render for CopySource<X> {
4889    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4890        match self {
4891            Self::Table { table, columns, .. } => {
4892                table.render(ctx, f)?;
4893                if !columns.is_empty() {
4894                    f.write_str(" (")?;
4895                    render_ident_list(columns, ctx, f)?;
4896                    f.write_str(")")?;
4897                }
4898                Ok(())
4899            }
4900            Self::Query { query, .. } => {
4901                f.write_str("(")?;
4902                query.render(ctx, f)?;
4903                f.write_str(")")
4904            }
4905        }
4906    }
4907}
4908
4909impl Render for CopyDirection {
4910    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4911        f.write_str(match self {
4912            Self::From => "FROM",
4913            Self::To => "TO",
4914        })
4915    }
4916}
4917
4918impl Render for CopyTarget {
4919    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4920        match self {
4921            Self::File { path, .. } => path.render(ctx, f),
4922            Self::Stdin { .. } => f.write_str("STDIN"),
4923            Self::Stdout { .. } => f.write_str("STDOUT"),
4924            Self::Program { command, .. } => {
4925                f.write_str("PROGRAM ")?;
4926                command.render(ctx, f)
4927            }
4928        }
4929    }
4930}
4931
4932impl Render for CopyOption {
4933    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4934        self.name.render(ctx, f)?;
4935        if let Some(value) = &self.value {
4936            f.write_str(" ")?;
4937            value.render(ctx, f)?;
4938        }
4939        Ok(())
4940    }
4941}
4942
4943impl Render for CopyOptionValue {
4944    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4945        match self {
4946            Self::Word { word, .. } => word.render(ctx, f),
4947            Self::String { value, .. } => value.render(ctx, f),
4948            Self::Number { value, .. } => value.render(ctx, f),
4949            Self::Star { .. } => f.write_str("*"),
4950            Self::List { values, .. } => {
4951                f.write_str("(")?;
4952                render_comma_separated(values, ctx, f)?;
4953                f.write_str(")")
4954            }
4955            // The Snowflake `FILE_FORMAT = (TYPE = CSV ...)` nested list: space-separated
4956            // `key = value` pairs (the `= (...)` argument spelling), never PostgreSQL's
4957            // comma-separated `List` shape above.
4958            Self::OptionList { options, .. } => {
4959                f.write_str("(")?;
4960                render_copy_into_options(options, ctx, f)?;
4961                f.write_str(")")
4962            }
4963            Self::Force { kind, columns, .. } => {
4964                kind.render(ctx, f)?;
4965                f.write_str(" ")?;
4966                // An empty column list is the `*` (all-columns) form.
4967                if columns.is_empty() {
4968                    f.write_str("*")
4969                } else {
4970                    render_ident_list(columns, ctx, f)
4971                }
4972            }
4973        }
4974    }
4975}
4976
4977impl Render for ForceKind {
4978    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4979        f.write_str(match self {
4980            Self::Quote => "QUOTE",
4981            Self::Null => "NULL",
4982            Self::NotNull => "NOT NULL",
4983        })
4984    }
4985}
4986
4987/// Render the Snowflake `COPY INTO` option list: space-separated `<name> = <value>`
4988/// pairs (the `KEY = VALUE` spelling), reused for both the top-level option list and
4989/// the nested [`CopyOptionValue::OptionList`] argument. A valueless option — which
4990/// Snowflake's grammar never emits, but the shared [`CopyOption`] type permits —
4991/// renders as the bare name.
4992fn render_copy_into_options(
4993    options: &[CopyOption],
4994    ctx: &RenderCtx<'_>,
4995    f: &mut fmt::Formatter<'_>,
4996) -> fmt::Result {
4997    for (i, option) in options.iter().enumerate() {
4998        if i > 0 {
4999            f.write_str(" ")?;
5000        }
5001        option.name.render(ctx, f)?;
5002        if let Some(value) = &option.value {
5003            f.write_str(" = ")?;
5004            value.render(ctx, f)?;
5005        }
5006    }
5007    Ok(())
5008}
5009
5010impl<X: Extension + Render> Render for CopyIntoStatement<X> {
5011    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5012        f.write_str("COPY INTO ")?;
5013        self.target.render(ctx, f)?;
5014        f.write_str(" FROM ")?;
5015        self.source.render(ctx, f)?;
5016        if !self.options.is_empty() {
5017            f.write_str(" ")?;
5018            render_copy_into_options(&self.options, ctx, f)?;
5019        }
5020        Ok(())
5021    }
5022}
5023
5024impl Render for ExportStatement {
5025    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5026        f.write_str("EXPORT DATABASE ")?;
5027        // A named catalogue reconstructs the required `<db> TO` before the path; the bare
5028        // form renders just the path.
5029        if let Some(database) = &self.database {
5030            database.render(ctx, f)?;
5031            f.write_str(" TO ")?;
5032        }
5033        self.path.render(ctx, f)?;
5034        // The option trailer mirrors `COPY`'s two spellings, minus the leading `WITH` the
5035        // `EXPORT` grammar omits: the parenthesized form renders bare `(...)`, the legacy
5036        // form space-separated. An empty list renders nothing in either spelling.
5037        if !self.options.is_empty() {
5038            if self.parenthesized {
5039                f.write_str(" (")?;
5040                render_comma_separated(&self.options, ctx, f)?;
5041                f.write_str(")")?;
5042            } else {
5043                for option in &self.options {
5044                    f.write_str(" ")?;
5045                    option.render(ctx, f)?;
5046                }
5047            }
5048        }
5049        Ok(())
5050    }
5051}
5052
5053impl Render for ImportStatement {
5054    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5055        f.write_str("IMPORT DATABASE ")?;
5056        self.path.render(ctx, f)
5057    }
5058}
5059
5060impl Render for CopyIntoTarget {
5061    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5062        match self {
5063            Self::Table { table, columns, .. } => {
5064                table.render(ctx, f)?;
5065                if !columns.is_empty() {
5066                    f.write_str(" (")?;
5067                    render_ident_list(columns, ctx, f)?;
5068                    f.write_str(")")?;
5069                }
5070                Ok(())
5071            }
5072            Self::External { location, .. } => location.render(ctx, f),
5073            Self::Stage { reference, .. } => reference.render(ctx, f),
5074        }
5075    }
5076}
5077
5078impl<X: Extension + Render> Render for CopyIntoSource<X> {
5079    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5080        match self {
5081            Self::Table { table, .. } => table.render(ctx, f),
5082            Self::External { location, .. } => location.render(ctx, f),
5083            Self::Stage { reference, .. } => reference.render(ctx, f),
5084            Self::Query { query, .. } => {
5085                f.write_str("(")?;
5086                query.render(ctx, f)?;
5087                f.write_str(")")
5088            }
5089        }
5090    }
5091}
5092
5093impl Render for ExplainKeyword {
5094    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5095        f.write_str(match self {
5096            Self::Explain => "EXPLAIN",
5097            Self::Describe => "DESCRIBE",
5098            Self::Desc => "DESC",
5099        })
5100    }
5101}
5102
5103impl<X: Extension + Render> Render for ExplainStatement<X> {
5104    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5105        self.spelling.render(ctx, f)?;
5106        if self.parenthesized {
5107            f.write_str(" (")?;
5108            render_comma_separated(&self.options, ctx, f)?;
5109            f.write_str(")")?;
5110        } else {
5111            // The legacy keyword prefix: each option is a bare space-separated word.
5112            for option in &self.options {
5113                f.write_str(" ")?;
5114                option.render(ctx, f)?;
5115            }
5116        }
5117        f.write_str(" ")?;
5118        self.statement.render(ctx, f)
5119    }
5120}
5121
5122impl Render for ExplainOption {
5123    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5124        match self {
5125            Self::Analyze { value, .. } => {
5126                render_explain_word_option("ANALYZE", value.as_ref(), ctx, f)
5127            }
5128            Self::Verbose { value, .. } => {
5129                render_explain_word_option("VERBOSE", value.as_ref(), ctx, f)
5130            }
5131            Self::Format { format, .. } => {
5132                f.write_str("FORMAT ")?;
5133                format.render(ctx, f)
5134            }
5135            Self::Other { name, value, .. } => {
5136                name.render(ctx, f)?;
5137                if let Some(value) = value {
5138                    f.write_str(" ")?;
5139                    value.render(ctx, f)?;
5140                }
5141                Ok(())
5142            }
5143        }
5144    }
5145}
5146
5147/// Render a built-in `EXPLAIN` option keyword with its optional argument word.
5148fn render_explain_word_option(
5149    keyword: &str,
5150    value: Option<&Ident>,
5151    ctx: &RenderCtx<'_>,
5152    f: &mut fmt::Formatter<'_>,
5153) -> fmt::Result {
5154    f.write_str(keyword)?;
5155    if let Some(value) = value {
5156        f.write_str(" ")?;
5157        value.render(ctx, f)?;
5158    }
5159    Ok(())
5160}
5161
5162impl Render for ExplainFormat {
5163    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5164        f.write_str(match self {
5165            Self::Text => "TEXT",
5166            Self::Xml => "XML",
5167            Self::Json => "JSON",
5168            Self::Yaml => "YAML",
5169        })
5170    }
5171}
5172
5173impl Render for DescribeStatement {
5174    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5175        self.keyword.render(ctx, f)?;
5176        f.write_str(" ")?;
5177        self.table.render(ctx, f)?;
5178        if let Some(column) = &self.column {
5179            f.write_str(" ")?;
5180            column.render(ctx, f)?;
5181        }
5182        Ok(())
5183    }
5184}
5185
5186impl Render for DescribeColumn {
5187    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5188        match self {
5189            Self::Name { name, .. } => name.render(ctx, f),
5190            Self::Wild { pattern, .. } => pattern.render(ctx, f),
5191        }
5192    }
5193}
5194
5195impl<X: Extension + Render> Render for ShowStatement<X> {
5196    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5197        self.target.render(ctx, f)
5198    }
5199}
5200
5201impl<X: Extension + Render> Render for ShowTarget<X> {
5202    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5203        match self {
5204            Self::Tables {
5205                extended,
5206                full,
5207                all,
5208                from,
5209                filter,
5210                ..
5211            } => {
5212                f.write_str("SHOW ")?;
5213                if *extended {
5214                    f.write_str("EXTENDED ")?;
5215                }
5216                if *full {
5217                    f.write_str("FULL ")?;
5218                }
5219                if *all {
5220                    f.write_str("ALL ")?;
5221                }
5222                f.write_str("TABLES")?;
5223                if let Some(from) = from {
5224                    f.write_str(" ")?;
5225                    from.render(ctx, f)?;
5226                }
5227                if let Some(filter) = filter {
5228                    f.write_str(" ")?;
5229                    filter.render(ctx, f)?;
5230                }
5231                Ok(())
5232            }
5233            Self::Columns {
5234                extended,
5235                full,
5236                spelling,
5237                table,
5238                database,
5239                filter,
5240                ..
5241            } => {
5242                f.write_str("SHOW ")?;
5243                if *extended {
5244                    f.write_str("EXTENDED ")?;
5245                }
5246                if *full {
5247                    f.write_str("FULL ")?;
5248                }
5249                f.write_str(match spelling {
5250                    ShowColumnsSpelling::Columns => "COLUMNS ",
5251                    ShowColumnsSpelling::Fields => "FIELDS ",
5252                })?;
5253                table.render(ctx, f)?;
5254                if let Some(database) = database {
5255                    f.write_str(" ")?;
5256                    database.render(ctx, f)?;
5257                }
5258                if let Some(filter) = filter {
5259                    f.write_str(" ")?;
5260                    filter.render(ctx, f)?;
5261                }
5262                Ok(())
5263            }
5264            Self::Create {
5265                kind,
5266                name,
5267                if_not_exists,
5268                ..
5269            } => {
5270                f.write_str("SHOW CREATE ")?;
5271                f.write_str(match kind {
5272                    ShowCreateKind::Table => "TABLE ",
5273                    ShowCreateKind::View => "VIEW ",
5274                    ShowCreateKind::Database { schema: false } => "DATABASE ",
5275                    ShowCreateKind::Database { schema: true } => "SCHEMA ",
5276                    ShowCreateKind::Event => "EVENT ",
5277                    ShowCreateKind::Procedure => "PROCEDURE ",
5278                    ShowCreateKind::Function => "FUNCTION ",
5279                    ShowCreateKind::Trigger => "TRIGGER ",
5280                })?;
5281                if *if_not_exists {
5282                    f.write_str("IF NOT EXISTS ")?;
5283                }
5284                name.render(ctx, f)
5285            }
5286            Self::Functions {
5287                kind, from, filter, ..
5288            } => {
5289                f.write_str("SHOW ")?;
5290                if let Some(kind) = kind {
5291                    f.write_str(match kind {
5292                        ShowFunctionsScope::User => "USER ",
5293                        ShowFunctionsScope::System => "SYSTEM ",
5294                        ShowFunctionsScope::All => "ALL ",
5295                    })?;
5296                }
5297                f.write_str("FUNCTIONS")?;
5298                if let Some(from) = from {
5299                    f.write_str(" ")?;
5300                    from.render(ctx, f)?;
5301                }
5302                if let Some(filter) = filter {
5303                    f.write_str(" ")?;
5304                    filter.render(ctx, f)?;
5305                }
5306                Ok(())
5307            }
5308            Self::RoutineStatus { kind, filter, .. } => {
5309                f.write_str(match kind {
5310                    ShowRoutineKind::Function => "SHOW FUNCTION STATUS",
5311                    ShowRoutineKind::Procedure => "SHOW PROCEDURE STATUS",
5312                })?;
5313                if let Some(filter) = filter {
5314                    f.write_str(" ")?;
5315                    filter.render(ctx, f)?;
5316                }
5317                Ok(())
5318            }
5319            Self::Listing {
5320                kind, from, filter, ..
5321            } => {
5322                f.write_str("SHOW ")?;
5323                match kind {
5324                    ShowListing::Databases { schemas } => {
5325                        f.write_str(if *schemas { "SCHEMAS" } else { "DATABASES" })?;
5326                    }
5327                    ShowListing::CharacterSet { charset } => {
5328                        f.write_str(if *charset { "CHARSET" } else { "CHARACTER SET" })?;
5329                    }
5330                    ShowListing::Collation => f.write_str("COLLATION")?,
5331                    ShowListing::Status { scope } => {
5332                        render_show_scope(*scope, f)?;
5333                        f.write_str("STATUS")?;
5334                    }
5335                    ShowListing::Variables { scope } => {
5336                        render_show_scope(*scope, f)?;
5337                        f.write_str("VARIABLES")?;
5338                    }
5339                    ShowListing::Events => f.write_str("EVENTS")?,
5340                    ShowListing::TableStatus => f.write_str("TABLE STATUS")?,
5341                    ShowListing::OpenTables => f.write_str("OPEN TABLES")?,
5342                    ShowListing::Triggers { full } => {
5343                        if *full {
5344                            f.write_str("FULL ")?;
5345                        }
5346                        f.write_str("TRIGGERS")?;
5347                    }
5348                }
5349                if let Some(from) = from {
5350                    f.write_str(" ")?;
5351                    from.render(ctx, f)?;
5352                }
5353                if let Some(filter) = filter {
5354                    f.write_str(" ")?;
5355                    filter.render(ctx, f)?;
5356                }
5357                Ok(())
5358            }
5359            Self::Bare { kind, .. } => {
5360                f.write_str("SHOW ")?;
5361                f.write_str(match kind {
5362                    ShowBare::Plugins => "PLUGINS",
5363                    ShowBare::Engines { storage: true } => "STORAGE ENGINES",
5364                    ShowBare::Engines { storage: false } => "ENGINES",
5365                    ShowBare::Privileges => "PRIVILEGES",
5366                    ShowBare::Profiles => "PROFILES",
5367                    ShowBare::Processlist { full: true } => "FULL PROCESSLIST",
5368                    ShowBare::Processlist { full: false } => "PROCESSLIST",
5369                    ShowBare::BinaryLogs => "BINARY LOGS",
5370                    ShowBare::Replicas => "REPLICAS",
5371                    ShowBare::BinaryLogStatus => "BINARY LOG STATUS",
5372                })
5373            }
5374            Self::Index {
5375                spelling,
5376                extended,
5377                table,
5378                database,
5379                filter,
5380                ..
5381            } => {
5382                f.write_str("SHOW ")?;
5383                if *extended {
5384                    f.write_str("EXTENDED ")?;
5385                }
5386                f.write_str(match spelling {
5387                    ShowIndexSpelling::Index => "INDEX ",
5388                    ShowIndexSpelling::Indexes => "INDEXES ",
5389                    ShowIndexSpelling::Keys => "KEYS ",
5390                })?;
5391                table.render(ctx, f)?;
5392                if let Some(database) = database {
5393                    f.write_str(" ")?;
5394                    database.render(ctx, f)?;
5395                }
5396                if let Some(filter) = filter {
5397                    f.write_str(" ")?;
5398                    filter.render(ctx, f)?;
5399                }
5400                Ok(())
5401            }
5402            Self::Engine {
5403                engine, artifact, ..
5404            } => {
5405                f.write_str("SHOW ENGINE ")?;
5406                match engine {
5407                    Some(name) => name.render(ctx, f)?,
5408                    None => f.write_str("ALL")?,
5409                }
5410                f.write_str(match artifact {
5411                    ShowEngineArtifact::Status => " STATUS",
5412                    ShowEngineArtifact::Mutex => " MUTEX",
5413                    ShowEngineArtifact::Logs => " LOGS",
5414                })
5415            }
5416            Self::ReplicaStatus { channel, .. } => {
5417                f.write_str("SHOW REPLICA STATUS")?;
5418                if let Some(channel) = channel {
5419                    f.write_str(" FOR CHANNEL ")?;
5420                    channel.render(ctx, f)?;
5421                }
5422                Ok(())
5423            }
5424            Self::Diagnostics {
5425                kind, count, limit, ..
5426            } => {
5427                f.write_str("SHOW ")?;
5428                if *count {
5429                    f.write_str("COUNT(*) ")?;
5430                }
5431                f.write_str(match kind {
5432                    ShowDiagnosticKind::Warnings => "WARNINGS",
5433                    ShowDiagnosticKind::Errors => "ERRORS",
5434                })?;
5435                if let Some(limit) = limit {
5436                    f.write_str(" ")?;
5437                    render_show_limit(limit, ctx, f)?;
5438                }
5439                Ok(())
5440            }
5441            Self::RoutineCode { kind, name, .. } => {
5442                f.write_str(match kind {
5443                    ShowRoutineKind::Function => "SHOW FUNCTION CODE ",
5444                    ShowRoutineKind::Procedure => "SHOW PROCEDURE CODE ",
5445                })?;
5446                name.render(ctx, f)
5447            }
5448            Self::Grants {
5449                user, using_roles, ..
5450            } => {
5451                f.write_str("SHOW GRANTS")?;
5452                if let Some(user) = user {
5453                    f.write_str(" FOR ")?;
5454                    user.render(ctx, f)?;
5455                    if !using_roles.is_empty() {
5456                        f.write_str(" USING ")?;
5457                        render_comma_separated(using_roles, ctx, f)?;
5458                    }
5459                }
5460                Ok(())
5461            }
5462            Self::CreateUser { user, .. } => {
5463                f.write_str("SHOW CREATE USER ")?;
5464                user.render(ctx, f)
5465            }
5466            Self::Profile {
5467                types,
5468                query,
5469                limit,
5470                ..
5471            } => {
5472                f.write_str("SHOW PROFILE")?;
5473                for (index, ty) in types.iter().enumerate() {
5474                    f.write_str(if index == 0 { " " } else { ", " })?;
5475                    f.write_str(ty.keyword())?;
5476                }
5477                if let Some(query) = query {
5478                    f.write_str(" FOR QUERY ")?;
5479                    query.render(ctx, f)?;
5480                }
5481                if let Some(limit) = limit {
5482                    f.write_str(" ")?;
5483                    render_show_limit(limit, ctx, f)?;
5484                }
5485                Ok(())
5486            }
5487            Self::LogEvents {
5488                relay,
5489                log_name,
5490                position,
5491                limit,
5492                channel,
5493                ..
5494            } => {
5495                f.write_str(if *relay {
5496                    "SHOW RELAYLOG EVENTS"
5497                } else {
5498                    "SHOW BINLOG EVENTS"
5499                })?;
5500                if let Some(log_name) = log_name {
5501                    f.write_str(" IN ")?;
5502                    log_name.render(ctx, f)?;
5503                }
5504                if let Some(position) = position {
5505                    f.write_str(" FROM ")?;
5506                    position.render(ctx, f)?;
5507                }
5508                if let Some(limit) = limit {
5509                    f.write_str(" ")?;
5510                    render_show_limit(limit, ctx, f)?;
5511                }
5512                if let Some(channel) = channel {
5513                    f.write_str(" FOR CHANNEL ")?;
5514                    channel.render(ctx, f)?;
5515                }
5516                Ok(())
5517            }
5518        }
5519    }
5520}
5521
5522impl ShowProfileType {
5523    /// The canonical keyword spelling of this profile type.
5524    fn keyword(self) -> &'static str {
5525        match self {
5526            Self::All => "ALL",
5527            Self::BlockIo => "BLOCK IO",
5528            Self::ContextSwitches => "CONTEXT SWITCHES",
5529            Self::Cpu => "CPU",
5530            Self::Ipc => "IPC",
5531            Self::Memory => "MEMORY",
5532            Self::PageFaults => "PAGE FAULTS",
5533            Self::Source => "SOURCE",
5534            Self::Swaps => "SWAPS",
5535        }
5536    }
5537}
5538
5539/// Render a [`ShowLimit`] — the shared `SHOW`-family `LIMIT` tail — as `LIMIT <row_count>`,
5540/// `LIMIT <offset>, <row_count>`, or `LIMIT <row_count> OFFSET <offset>`, matching the written
5541/// offset form. No surrounding whitespace; the caller writes the leading space.
5542fn render_show_limit(
5543    limit: &ShowLimit,
5544    ctx: &RenderCtx<'_>,
5545    f: &mut fmt::Formatter<'_>,
5546) -> fmt::Result {
5547    f.write_str("LIMIT ")?;
5548    match &limit.offset {
5549        Some(offset) if limit.offset_keyword => {
5550            limit.row_count.render(ctx, f)?;
5551            f.write_str(" OFFSET ")?;
5552            offset.render(ctx, f)
5553        }
5554        Some(offset) => {
5555            offset.render(ctx, f)?;
5556            f.write_str(", ")?;
5557            limit.row_count.render(ctx, f)
5558        }
5559        None => limit.row_count.render(ctx, f),
5560    }
5561}
5562
5563/// Render an optional `GLOBAL`/`SESSION`/`LOCAL` scope keyword (trailing space included) for
5564/// the `SHOW … STATUS` / `SHOW … VARIABLES` listings; nothing when the scope is `None`.
5565fn render_show_scope(scope: Option<ShowScope>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5566    if let Some(scope) = scope {
5567        f.write_str(match scope {
5568            ShowScope::Global => "GLOBAL ",
5569            ShowScope::Session => "SESSION ",
5570            ShowScope::Local => "LOCAL ",
5571        })?;
5572    }
5573    Ok(())
5574}
5575
5576impl Render for ShowFunctionsFilter {
5577    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5578        match self {
5579            Self::Name { like, name, .. } => {
5580                if *like {
5581                    f.write_str("LIKE ")?;
5582                }
5583                name.render(ctx, f)
5584            }
5585            Self::Regex { like, pattern, .. } => {
5586                if *like {
5587                    f.write_str("LIKE ")?;
5588                }
5589                pattern.render(ctx, f)
5590            }
5591        }
5592    }
5593}
5594
5595impl Render for ShowFrom {
5596    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5597        f.write_str(match self.keyword {
5598            ShowFromKeyword::From => "FROM ",
5599            ShowFromKeyword::In => "IN ",
5600        })?;
5601        self.name.render(ctx, f)
5602    }
5603}
5604
5605impl<X: Extension + Render> Render for ShowFilter<X> {
5606    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5607        match self {
5608            Self::Like { pattern, .. } => {
5609                f.write_str("LIKE ")?;
5610                pattern.render(ctx, f)
5611            }
5612            Self::Where { predicate, .. } => {
5613                f.write_str("WHERE ")?;
5614                predicate.render(ctx, f)
5615            }
5616        }
5617    }
5618}
5619
5620impl<X: Extension + Render> Render for KillStatement<X> {
5621    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5622        f.write_str("KILL")?;
5623        match self.target {
5624            KillTarget::Unspecified => {}
5625            KillTarget::Connection => f.write_str(" CONNECTION")?,
5626            KillTarget::Query => f.write_str(" QUERY")?,
5627        }
5628        f.write_str(" ")?;
5629        self.id.render(ctx, f)
5630    }
5631}
5632
5633impl<X: Extension + Render> Render for InstallStatement<X> {
5634    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5635        match self {
5636            InstallStatement::Plugin { name, soname, .. } => {
5637                f.write_str("INSTALL PLUGIN ")?;
5638                name.render(ctx, f)?;
5639                f.write_str(" SONAME ")?;
5640                soname.render(ctx, f)
5641            }
5642            InstallStatement::Component { urns, set, .. } => {
5643                f.write_str("INSTALL COMPONENT ")?;
5644                render_comma_separated(urns, ctx, f)?;
5645                if !set.is_empty() {
5646                    f.write_str(" SET ")?;
5647                    render_comma_separated(set, ctx, f)?;
5648                }
5649                Ok(())
5650            }
5651        }
5652    }
5653}
5654
5655impl Render for UninstallStatement {
5656    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5657        match self {
5658            UninstallStatement::Plugin { name, .. } => {
5659                f.write_str("UNINSTALL PLUGIN ")?;
5660                name.render(ctx, f)
5661            }
5662            UninstallStatement::Component { urns, .. } => {
5663                f.write_str("UNINSTALL COMPONENT ")?;
5664                render_comma_separated(urns, ctx, f)
5665            }
5666        }
5667    }
5668}
5669
5670impl<X: Extension + Render> Render for InstallComponentSetElement<X> {
5671    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5672        match self.scope {
5673            None => {}
5674            Some(InstallComponentSetScope::Global) => f.write_str("GLOBAL ")?,
5675            Some(InstallComponentSetScope::Persist) => f.write_str("PERSIST ")?,
5676        }
5677        self.name.render(ctx, f)?;
5678        render_mysql_set_assignment(self.assignment, ctx, f)?;
5679        self.value.render(ctx, f)
5680    }
5681}
5682
5683impl<X: Extension + Render> Render for InstallComponentSetValue<X> {
5684    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5685        match self {
5686            InstallComponentSetValue::On { .. } => f.write_str("ON"),
5687            InstallComponentSetValue::Expr { expr, .. } => expr.render(ctx, f),
5688        }
5689    }
5690}
5691
5692impl<X: Extension + Render> Render for HandlerStatement<X> {
5693    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5694        f.write_str("HANDLER ")?;
5695        self.table.render(ctx, f)?;
5696        f.write_str(" ")?;
5697        self.operation.render(ctx, f)
5698    }
5699}
5700
5701impl<X: Extension + Render> Render for HandlerOperation<X> {
5702    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5703        match self {
5704            HandlerOperation::Open {
5705                alias, as_keyword, ..
5706            } => {
5707                f.write_str("OPEN")?;
5708                if let Some(alias) = alias {
5709                    // `AS` is optional noise; the tag round-trips whichever was written.
5710                    f.write_str(if *as_keyword { " AS " } else { " " })?;
5711                    alias.render(ctx, f)?;
5712                }
5713                Ok(())
5714            }
5715            HandlerOperation::Close { .. } => f.write_str("CLOSE"),
5716            HandlerOperation::Read {
5717                selector,
5718                selection,
5719                limit,
5720                ..
5721            } => {
5722                f.write_str("READ ")?;
5723                selector.render(ctx, f)?;
5724                if let Some(selection) = selection {
5725                    f.write_str(" WHERE ")?;
5726                    selection.render(ctx, f)?;
5727                }
5728                if let Some(limit) = limit {
5729                    f.write_str(" ")?;
5730                    limit.render(ctx, f)?;
5731                }
5732                Ok(())
5733            }
5734        }
5735    }
5736}
5737
5738impl<X: Extension + Render> Render for HandlerReadSelector<X> {
5739    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5740        match self {
5741            HandlerReadSelector::Scan { direction, .. } => f.write_str(match direction {
5742                HandlerScanDirection::First => "FIRST",
5743                HandlerScanDirection::Next => "NEXT",
5744            }),
5745            HandlerReadSelector::Index {
5746                index, direction, ..
5747            } => {
5748                index.render(ctx, f)?;
5749                f.write_str(match direction {
5750                    HandlerIndexDirection::First => " FIRST",
5751                    HandlerIndexDirection::Next => " NEXT",
5752                    HandlerIndexDirection::Prev => " PREV",
5753                    HandlerIndexDirection::Last => " LAST",
5754                })
5755            }
5756            HandlerReadSelector::Key {
5757                index,
5758                comparison,
5759                key,
5760                ..
5761            } => {
5762                index.render(ctx, f)?;
5763                f.write_str(match comparison {
5764                    HandlerKeyComparison::Eq => " = (",
5765                    HandlerKeyComparison::GreaterOrEqual => " >= (",
5766                    HandlerKeyComparison::LessOrEqual => " <= (",
5767                    HandlerKeyComparison::Greater => " > (",
5768                    HandlerKeyComparison::Less => " < (",
5769                })?;
5770                render_comma_separated(key, ctx, f)?;
5771                f.write_str(")")
5772            }
5773        }
5774    }
5775}
5776
5777impl Render for CloneStatement {
5778    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5779        match self {
5780            CloneStatement::Local { data_directory, .. } => {
5781                f.write_str("CLONE LOCAL ")?;
5782                data_directory.render(ctx, f)
5783            }
5784            CloneStatement::Instance {
5785                source,
5786                port,
5787                password,
5788                data_directory,
5789                ssl,
5790                ..
5791            } => {
5792                f.write_str("CLONE INSTANCE FROM ")?;
5793                source.render(ctx, f)?;
5794                f.write_str(":")?;
5795                port.render(ctx, f)?;
5796                f.write_str(" IDENTIFIED BY ")?;
5797                password.render(ctx, f)?;
5798                if let Some(data_directory) = data_directory {
5799                    f.write_str(" ")?;
5800                    data_directory.render(ctx, f)?;
5801                }
5802                match ssl {
5803                    CloneSsl::Unspecified => {}
5804                    CloneSsl::Require => f.write_str(" REQUIRE SSL")?,
5805                    CloneSsl::RequireNo => f.write_str(" REQUIRE NO SSL")?,
5806                }
5807                Ok(())
5808            }
5809        }
5810    }
5811}
5812
5813impl Render for CloneDataDirectory {
5814    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5815        f.write_str(if self.equals {
5816            "DATA DIRECTORY = "
5817        } else {
5818            "DATA DIRECTORY "
5819        })?;
5820        self.path.render(ctx, f)
5821    }
5822}
5823
5824impl Render for ImportTableStatement {
5825    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5826        f.write_str("IMPORT TABLE FROM ")?;
5827        render_comma_separated(&self.files, ctx, f)
5828    }
5829}
5830
5831impl Render for HelpStatement {
5832    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5833        f.write_str("HELP ")?;
5834        self.topic.render(ctx, f)
5835    }
5836}
5837
5838impl Render for BinlogStatement {
5839    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5840        f.write_str("BINLOG ")?;
5841        self.event.render(ctx, f)
5842    }
5843}
5844
5845impl Render for PragmaStatement {
5846    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5847        f.write_str("PRAGMA ")?;
5848        self.name.render(ctx, f)?;
5849        if let Some(value) = &self.value {
5850            // The `parenthesized` surface tag picks the spelling; SQLite writes no
5851            // space before the call form's `(`.
5852            if self.parenthesized {
5853                f.write_str("(")?;
5854                value.render(ctx, f)?;
5855                f.write_str(")")?;
5856            } else {
5857                f.write_str(" = ")?;
5858                value.render(ctx, f)?;
5859            }
5860        }
5861        Ok(())
5862    }
5863}
5864
5865impl<X: Extension + Render> Render for AttachStatement<X> {
5866    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5867        f.write_str("ATTACH ")?;
5868        if self.database_keyword {
5869            f.write_str("DATABASE ")?;
5870        }
5871        self.target.render(ctx, f)?;
5872        f.write_str(" AS ")?;
5873        self.schema.render(ctx, f)
5874    }
5875}
5876
5877impl Render for DetachStatement {
5878    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5879        f.write_str("DETACH ")?;
5880        if self.database_keyword {
5881            f.write_str("DATABASE ")?;
5882        }
5883        if self.if_exists {
5884            f.write_str("IF EXISTS ")?;
5885        }
5886        self.schema.render(ctx, f)
5887    }
5888}
5889
5890impl Render for CheckpointStatement {
5891    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5892        if self.force {
5893            f.write_str("FORCE ")?;
5894        }
5895        f.write_str("CHECKPOINT")?;
5896        if let Some(database) = &self.database {
5897            f.write_str(" ")?;
5898            database.render(ctx, f)?;
5899        }
5900        Ok(())
5901    }
5902}
5903
5904impl Render for LoadStatement {
5905    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5906        f.write_str("LOAD ")?;
5907        self.target.render(ctx, f)
5908    }
5909}
5910
5911impl Render for LoadTarget {
5912    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5913        match self {
5914            Self::Name { name, .. } => name.render(ctx, f),
5915            Self::Path { path, .. } => path.render(ctx, f),
5916        }
5917    }
5918}
5919
5920impl Render for UseStatement {
5921    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5922        f.write_str("USE ")?;
5923        self.name.render(ctx, f)
5924    }
5925}
5926
5927impl Render for UpdateExtensionsStatement {
5928    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5929        f.write_str("UPDATE EXTENSIONS")?;
5930        // An empty list is the bare `UPDATE EXTENSIONS` (refresh all); a written list is
5931        // never empty (`UPDATE EXTENSIONS ()` is a DuckDB parser error), so a non-empty
5932        // vector renders the parenthesized form.
5933        if !self.extensions.is_empty() {
5934            f.write_str(" (")?;
5935            render_comma_separated(&self.extensions, ctx, f)?;
5936            f.write_str(")")?;
5937        }
5938        Ok(())
5939    }
5940}
5941
5942impl<X: Extension + Render> Render for VacuumStatement<X> {
5943    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5944        f.write_str("VACUUM")?;
5945        // DuckDB `ANALYZE` option precedes the operands; SQLite never sets it. The
5946        // parenthesized list canonicalizes to the single `(ANALYZE)` form (repeats carry
5947        // no meaning — see `VacuumAnalyze`).
5948        match self.analyze {
5949            Some(VacuumAnalyze::Keyword) => f.write_str(" ANALYZE")?,
5950            Some(VacuumAnalyze::Parenthesized) => f.write_str(" (ANALYZE)")?,
5951            None => {}
5952        }
5953        // The name operand: SQLite's single-ident schema or DuckDB's qualified table
5954        // (mutually exclusive by dialect).
5955        if let Some(schema) = &self.schema {
5956            f.write_str(" ")?;
5957            schema.render(ctx, f)?;
5958        }
5959        if let Some(table) = &self.table {
5960            f.write_str(" ")?;
5961            table.render(ctx, f)?;
5962        }
5963        // DuckDB column list, only alongside a table.
5964        if let Some(columns) = &self.columns {
5965            f.write_str(" (")?;
5966            render_comma_separated(columns, ctx, f)?;
5967            f.write_str(")")?;
5968        }
5969        // SQLite `INTO <expr>` tail.
5970        if let Some(into) = &self.into {
5971            f.write_str(" INTO ")?;
5972            into.render(ctx, f)?;
5973        }
5974        Ok(())
5975    }
5976}
5977
5978impl Render for ReindexStatement {
5979    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5980        f.write_str("REINDEX")?;
5981        if let Some(target) = &self.target {
5982            f.write_str(" ")?;
5983            target.render(ctx, f)?;
5984        }
5985        Ok(())
5986    }
5987}
5988
5989impl Render for AnalyzeStatement {
5990    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5991        f.write_str("ANALYZE")?;
5992        if let Some(target) = &self.target {
5993            f.write_str(" ")?;
5994            target.render(ctx, f)?;
5995        }
5996        // DuckDB column list, only alongside a target.
5997        if let Some(columns) = &self.columns {
5998            f.write_str(" (")?;
5999            render_comma_separated(columns, ctx, f)?;
6000            f.write_str(")")?;
6001        }
6002        Ok(())
6003    }
6004}
6005
6006impl Render for TableMaintenanceStatement {
6007    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6008        // Verb + the optional `NO_WRITE_TO_BINLOG | LOCAL` prefix (ANALYZE/OPTIMIZE/REPAIR).
6009        let (verb, prefix) = match &self.kind {
6010            TableMaintenanceKind::Analyze {
6011                no_write_to_binlog, ..
6012            } => ("ANALYZE", *no_write_to_binlog),
6013            TableMaintenanceKind::Check { .. } => ("CHECK", None),
6014            TableMaintenanceKind::Checksum { .. } => ("CHECKSUM", None),
6015            TableMaintenanceKind::Optimize {
6016                no_write_to_binlog, ..
6017            } => ("OPTIMIZE", *no_write_to_binlog),
6018            TableMaintenanceKind::Repair {
6019                no_write_to_binlog, ..
6020            } => ("REPAIR", *no_write_to_binlog),
6021        };
6022        f.write_str(verb)?;
6023        if let Some(prefix) = prefix {
6024            f.write_str(" ")?;
6025            f.write_str(no_write_to_binlog_keyword(prefix))?;
6026        }
6027        f.write_str(" ")?;
6028        f.write_str(table_keyword_str(self.table_keyword))?;
6029        f.write_str(" ")?;
6030        render_comma_separated(&self.tables, ctx, f)?;
6031        // The per-verb trailing options.
6032        match &self.kind {
6033            TableMaintenanceKind::Analyze {
6034                histogram: Some(histogram),
6035                ..
6036            } => {
6037                f.write_str(" ")?;
6038                histogram.render(ctx, f)?;
6039            }
6040            TableMaintenanceKind::Check { options, .. } => {
6041                for option in options {
6042                    f.write_str(" ")?;
6043                    f.write_str(check_table_option_keyword(*option))?;
6044                }
6045            }
6046            TableMaintenanceKind::Checksum {
6047                option: Some(option),
6048                ..
6049            } => {
6050                f.write_str(" ")?;
6051                f.write_str(checksum_table_option_keyword(*option))?;
6052            }
6053            TableMaintenanceKind::Repair { options, .. } => {
6054                for option in options {
6055                    f.write_str(" ")?;
6056                    f.write_str(repair_table_option_keyword(*option))?;
6057                }
6058            }
6059            TableMaintenanceKind::Analyze {
6060                histogram: None, ..
6061            }
6062            | TableMaintenanceKind::Checksum { option: None, .. }
6063            | TableMaintenanceKind::Optimize { .. } => {}
6064        }
6065        Ok(())
6066    }
6067}
6068
6069impl Render for AnalyzeHistogram {
6070    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6071        match self {
6072            AnalyzeHistogram::Update {
6073                columns, buckets, ..
6074            } => {
6075                f.write_str("UPDATE HISTOGRAM ON ")?;
6076                render_comma_separated(columns, ctx, f)?;
6077                if let Some(buckets) = buckets {
6078                    f.write_str(" WITH ")?;
6079                    buckets.render(ctx, f)?;
6080                    f.write_str(" BUCKETS")?;
6081                }
6082            }
6083            AnalyzeHistogram::Drop { columns, .. } => {
6084                f.write_str("DROP HISTOGRAM ON ")?;
6085                render_comma_separated(columns, ctx, f)?;
6086            }
6087        }
6088        Ok(())
6089    }
6090}
6091
6092impl Render for CacheIndexStatement {
6093    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6094        f.write_str("CACHE INDEX ")?;
6095        self.targets.render(ctx, f)?;
6096        f.write_str(" IN ")?;
6097        self.cache.render(ctx, f)
6098    }
6099}
6100
6101impl Render for CacheIndexTargets {
6102    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6103        match self {
6104            CacheIndexTargets::Tables { tables, .. } => render_comma_separated(tables, ctx, f),
6105            CacheIndexTargets::Partition {
6106                table,
6107                partition,
6108                keys,
6109                ..
6110            } => {
6111                table.render(ctx, f)?;
6112                f.write_str(" ")?;
6113                partition.render(ctx, f)?;
6114                if let Some(keys) = keys {
6115                    f.write_str(" ")?;
6116                    keys.render(ctx, f)?;
6117                }
6118                Ok(())
6119            }
6120        }
6121    }
6122}
6123
6124impl Render for CacheIndexTable {
6125    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6126        self.table.render(ctx, f)?;
6127        if let Some(keys) = &self.keys {
6128            f.write_str(" ")?;
6129            keys.render(ctx, f)?;
6130        }
6131        Ok(())
6132    }
6133}
6134
6135impl Render for LoadIndexStatement {
6136    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6137        f.write_str("LOAD INDEX INTO CACHE ")?;
6138        self.targets.render(ctx, f)
6139    }
6140}
6141
6142impl Render for LoadIndexTargets {
6143    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6144        match self {
6145            LoadIndexTargets::Tables { tables, .. } => render_comma_separated(tables, ctx, f),
6146            LoadIndexTargets::Partition {
6147                table,
6148                partition,
6149                keys,
6150                ignore_leaves,
6151                ..
6152            } => {
6153                table.render(ctx, f)?;
6154                f.write_str(" ")?;
6155                partition.render(ctx, f)?;
6156                if let Some(keys) = keys {
6157                    f.write_str(" ")?;
6158                    keys.render(ctx, f)?;
6159                }
6160                if *ignore_leaves {
6161                    f.write_str(" IGNORE LEAVES")?;
6162                }
6163                Ok(())
6164            }
6165        }
6166    }
6167}
6168
6169impl Render for LoadIndexTable {
6170    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6171        self.table.render(ctx, f)?;
6172        if let Some(keys) = &self.keys {
6173            f.write_str(" ")?;
6174            keys.render(ctx, f)?;
6175        }
6176        if self.ignore_leaves {
6177            f.write_str(" IGNORE LEAVES")?;
6178        }
6179        Ok(())
6180    }
6181}
6182
6183impl Render for CacheIndexKeyList {
6184    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6185        f.write_str(match self.keyword {
6186            CacheIndexKeyword::Index => "INDEX",
6187            CacheIndexKeyword::Key => "KEY",
6188        })?;
6189        f.write_str(" (")?;
6190        render_comma_separated(&self.keys, ctx, f)?;
6191        f.write_str(")")
6192    }
6193}
6194
6195impl Render for KeyCacheName {
6196    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6197        match self {
6198            KeyCacheName::Named { name, .. } => name.render(ctx, f),
6199            KeyCacheName::Default { .. } => f.write_str("DEFAULT"),
6200        }
6201    }
6202}
6203
6204impl Render for PartitionSelection {
6205    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6206        match self {
6207            PartitionSelection::All { .. } => f.write_str("PARTITION (ALL)"),
6208            PartitionSelection::Names { names, .. } => {
6209                f.write_str("PARTITION (")?;
6210                render_comma_separated(names, ctx, f)?;
6211                f.write_str(")")
6212            }
6213        }
6214    }
6215}
6216
6217impl Render for RenameStatement {
6218    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6219        match self {
6220            RenameStatement::Table {
6221                table_keyword,
6222                renames,
6223                ..
6224            } => {
6225                f.write_str("RENAME ")?;
6226                f.write_str(table_keyword_str(*table_keyword))?;
6227                f.write_str(" ")?;
6228                render_comma_separated(renames, ctx, f)
6229            }
6230            RenameStatement::User { renames, .. } => {
6231                f.write_str("RENAME USER ")?;
6232                render_comma_separated(renames, ctx, f)
6233            }
6234        }
6235    }
6236}
6237
6238impl Render for TableRename {
6239    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6240        self.from.render(ctx, f)?;
6241        f.write_str(" TO ")?;
6242        self.to.render(ctx, f)
6243    }
6244}
6245
6246impl Render for UserRename {
6247    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6248        self.from.render(ctx, f)?;
6249        f.write_str(" TO ")?;
6250        self.to.render(ctx, f)
6251    }
6252}
6253
6254impl Render for FlushStatement {
6255    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6256        f.write_str("FLUSH")?;
6257        if let Some(prefix) = self.no_write_to_binlog {
6258            f.write_str(" ")?;
6259            f.write_str(no_write_to_binlog_keyword(prefix))?;
6260        }
6261        f.write_str(" ")?;
6262        self.target.render(ctx, f)
6263    }
6264}
6265
6266impl Render for FlushTarget {
6267    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6268        match self {
6269            FlushTarget::Tables {
6270                table_keyword,
6271                tables,
6272                lock,
6273                ..
6274            } => {
6275                f.write_str(table_keyword_str(*table_keyword))?;
6276                if !tables.is_empty() {
6277                    f.write_str(" ")?;
6278                    render_comma_separated(tables, ctx, f)?;
6279                }
6280                if let Some(lock) = lock {
6281                    f.write_str(" ")?;
6282                    f.write_str(match lock {
6283                        FlushTablesLock::WithReadLock => "WITH READ LOCK",
6284                        FlushTablesLock::ForExport => "FOR EXPORT",
6285                    })?;
6286                }
6287                Ok(())
6288            }
6289            FlushTarget::Options { options, .. } => render_comma_separated(options, ctx, f),
6290        }
6291    }
6292}
6293
6294impl Render for FlushOption {
6295    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6296        match self {
6297            FlushOption::Privileges { .. } => f.write_str("PRIVILEGES"),
6298            FlushOption::Logs { .. } => f.write_str("LOGS"),
6299            FlushOption::BinaryLogs { .. } => f.write_str("BINARY LOGS"),
6300            FlushOption::EngineLogs { .. } => f.write_str("ENGINE LOGS"),
6301            FlushOption::ErrorLogs { .. } => f.write_str("ERROR LOGS"),
6302            FlushOption::GeneralLogs { .. } => f.write_str("GENERAL LOGS"),
6303            FlushOption::SlowLogs { .. } => f.write_str("SLOW LOGS"),
6304            FlushOption::RelayLogs { channel, .. } => {
6305                f.write_str("RELAY LOGS")?;
6306                if let Some(channel) = channel {
6307                    f.write_str(" FOR CHANNEL ")?;
6308                    channel.render(ctx, f)?;
6309                }
6310                Ok(())
6311            }
6312            FlushOption::Status { .. } => f.write_str("STATUS"),
6313            FlushOption::UserResources { .. } => f.write_str("USER_RESOURCES"),
6314            FlushOption::OptimizerCosts { .. } => f.write_str("OPTIMIZER_COSTS"),
6315        }
6316    }
6317}
6318
6319impl<X: Extension + Render> Render for PurgeStatement<X> {
6320    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6321        f.write_str("PURGE BINARY LOGS ")?;
6322        self.target.render(ctx, f)
6323    }
6324}
6325
6326impl<X: Extension + Render> Render for PurgeTarget<X> {
6327    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6328        match self {
6329            PurgeTarget::To { log, .. } => {
6330                f.write_str("TO ")?;
6331                log.render(ctx, f)
6332            }
6333            PurgeTarget::Before { datetime, .. } => {
6334                f.write_str("BEFORE ")?;
6335                datetime.render(ctx, f)
6336            }
6337        }
6338    }
6339}
6340
6341/// Render a trailing `[FOR CHANNEL '<name>']` suffix, shared by the four replication verbs
6342/// that carry one.
6343fn render_for_channel(
6344    channel: &Option<Literal>,
6345    ctx: &RenderCtx<'_>,
6346    f: &mut fmt::Formatter<'_>,
6347) -> fmt::Result {
6348    if let Some(channel) = channel {
6349        f.write_str(" FOR CHANNEL ")?;
6350        channel.render(ctx, f)?;
6351    }
6352    Ok(())
6353}
6354
6355/// Render one fixed-position `START REPLICA` connection option (`keyword` includes its
6356/// leading space and trailing ` = `), if present.
6357fn render_replica_connection_option(
6358    keyword: &str,
6359    value: &Option<Literal>,
6360    ctx: &RenderCtx<'_>,
6361    f: &mut fmt::Formatter<'_>,
6362) -> fmt::Result {
6363    if let Some(value) = value {
6364        f.write_str(keyword)?;
6365        value.render(ctx, f)?;
6366    }
6367    Ok(())
6368}
6369
6370impl Render for ReplicationStatement {
6371    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6372        match self {
6373            ReplicationStatement::ChangeSource {
6374                options, channel, ..
6375            } => {
6376                f.write_str("CHANGE REPLICATION SOURCE TO ")?;
6377                render_comma_separated(options, ctx, f)?;
6378                render_for_channel(channel, ctx, f)
6379            }
6380            ReplicationStatement::ChangeFilter { rules, channel, .. } => {
6381                f.write_str("CHANGE REPLICATION FILTER ")?;
6382                render_comma_separated(rules, ctx, f)?;
6383                render_for_channel(channel, ctx, f)
6384            }
6385            ReplicationStatement::StartReplica {
6386                threads,
6387                until,
6388                user,
6389                password,
6390                default_auth,
6391                plugin_dir,
6392                channel,
6393                ..
6394            } => {
6395                f.write_str("START REPLICA")?;
6396                if !threads.is_empty() {
6397                    f.write_str(" ")?;
6398                    render_comma_separated(threads, ctx, f)?;
6399                }
6400                if !until.is_empty() {
6401                    f.write_str(" UNTIL ")?;
6402                    render_comma_separated(until, ctx, f)?;
6403                }
6404                render_replica_connection_option(" USER = ", user, ctx, f)?;
6405                render_replica_connection_option(" PASSWORD = ", password, ctx, f)?;
6406                render_replica_connection_option(" DEFAULT_AUTH = ", default_auth, ctx, f)?;
6407                render_replica_connection_option(" PLUGIN_DIR = ", plugin_dir, ctx, f)?;
6408                render_for_channel(channel, ctx, f)
6409            }
6410            ReplicationStatement::StopReplica {
6411                threads, channel, ..
6412            } => {
6413                f.write_str("STOP REPLICA")?;
6414                if !threads.is_empty() {
6415                    f.write_str(" ")?;
6416                    render_comma_separated(threads, ctx, f)?;
6417                }
6418                render_for_channel(channel, ctx, f)
6419            }
6420            ReplicationStatement::StartGroupReplication { options, .. } => {
6421                f.write_str("START GROUP_REPLICATION")?;
6422                if !options.is_empty() {
6423                    f.write_str(" ")?;
6424                    render_comma_separated(options, ctx, f)?;
6425                }
6426                Ok(())
6427            }
6428            ReplicationStatement::StopGroupReplication { .. } => {
6429                f.write_str("STOP GROUP_REPLICATION")
6430            }
6431        }
6432    }
6433}
6434
6435impl Render for ChangeReplicationSourceOption {
6436    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6437        f.write_str(self.name.keyword())?;
6438        f.write_str(" = ")?;
6439        self.value.render(ctx, f)
6440    }
6441}
6442
6443impl Render for ChangeReplicationSourceOptionValue {
6444    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6445        match self {
6446            Self::String { value, .. } | Self::Number { value, .. } => value.render(ctx, f),
6447            Self::NullableString { value, .. } => match value {
6448                Some(value) => value.render(ctx, f),
6449                None => f.write_str("NULL"),
6450            },
6451            Self::User { account, .. } => match account {
6452                Some(account) => account.render(ctx, f),
6453                None => f.write_str("NULL"),
6454            },
6455            Self::ServerIds { ids, .. } => {
6456                f.write_str("(")?;
6457                render_comma_separated(ids, ctx, f)?;
6458                f.write_str(")")
6459            }
6460            Self::PrimaryKeyCheck { check, .. } => f.write_str(match check {
6461                RequirePrimaryKeyCheck::On => "ON",
6462                RequirePrimaryKeyCheck::Off => "OFF",
6463                RequirePrimaryKeyCheck::Stream => "STREAM",
6464                RequirePrimaryKeyCheck::Generate => "GENERATE",
6465            }),
6466            Self::AssignGtids { kind, uuid, .. } => match kind {
6467                AssignGtidsKind::Off => f.write_str("OFF"),
6468                AssignGtidsKind::Local => f.write_str("LOCAL"),
6469                AssignGtidsKind::Uuid => match uuid {
6470                    Some(uuid) => uuid.render(ctx, f),
6471                    None => Ok(()),
6472                },
6473            },
6474        }
6475    }
6476}
6477
6478impl Render for ReplicationFilterRule {
6479    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6480        match self {
6481            Self::DoDb { databases, .. } => {
6482                f.write_str("REPLICATE_DO_DB = (")?;
6483                render_comma_separated(databases, ctx, f)?;
6484                f.write_str(")")
6485            }
6486            Self::IgnoreDb { databases, .. } => {
6487                f.write_str("REPLICATE_IGNORE_DB = (")?;
6488                render_comma_separated(databases, ctx, f)?;
6489                f.write_str(")")
6490            }
6491            Self::DoTable { tables, .. } => {
6492                f.write_str("REPLICATE_DO_TABLE = (")?;
6493                render_comma_separated(tables, ctx, f)?;
6494                f.write_str(")")
6495            }
6496            Self::IgnoreTable { tables, .. } => {
6497                f.write_str("REPLICATE_IGNORE_TABLE = (")?;
6498                render_comma_separated(tables, ctx, f)?;
6499                f.write_str(")")
6500            }
6501            Self::WildDoTable { patterns, .. } => {
6502                f.write_str("REPLICATE_WILD_DO_TABLE = (")?;
6503                render_comma_separated(patterns, ctx, f)?;
6504                f.write_str(")")
6505            }
6506            Self::WildIgnoreTable { patterns, .. } => {
6507                f.write_str("REPLICATE_WILD_IGNORE_TABLE = (")?;
6508                render_comma_separated(patterns, ctx, f)?;
6509                f.write_str(")")
6510            }
6511            Self::RewriteDb { pairs, .. } => {
6512                f.write_str("REPLICATE_REWRITE_DB = (")?;
6513                render_comma_separated(pairs, ctx, f)?;
6514                f.write_str(")")
6515            }
6516        }
6517    }
6518}
6519
6520impl Render for RewriteDbPair {
6521    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6522        f.write_str("(")?;
6523        self.from.render(ctx, f)?;
6524        f.write_str(", ")?;
6525        self.to.render(ctx, f)?;
6526        f.write_str(")")
6527    }
6528}
6529
6530impl Render for ReplicaThreadOption {
6531    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6532        f.write_str(match self {
6533            Self::Sql { .. } => "SQL_THREAD",
6534            Self::Io {
6535                keyword: IoThreadKeyword::Io,
6536                ..
6537            } => "IO_THREAD",
6538            Self::Io {
6539                keyword: IoThreadKeyword::Relay,
6540                ..
6541            } => "RELAY_THREAD",
6542        })
6543    }
6544}
6545
6546impl Render for ReplicaUntilCondition {
6547    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6548        match self {
6549            Self::SourceLogFile { value, .. } => {
6550                f.write_str("SOURCE_LOG_FILE = ")?;
6551                value.render(ctx, f)
6552            }
6553            Self::SourceLogPos { value, .. } => {
6554                f.write_str("SOURCE_LOG_POS = ")?;
6555                value.render(ctx, f)
6556            }
6557            Self::RelayLogFile { value, .. } => {
6558                f.write_str("RELAY_LOG_FILE = ")?;
6559                value.render(ctx, f)
6560            }
6561            Self::RelayLogPos { value, .. } => {
6562                f.write_str("RELAY_LOG_POS = ")?;
6563                value.render(ctx, f)
6564            }
6565            Self::SqlBeforeGtids { value, .. } => {
6566                f.write_str("SQL_BEFORE_GTIDS = ")?;
6567                value.render(ctx, f)
6568            }
6569            Self::SqlAfterGtids { value, .. } => {
6570                f.write_str("SQL_AFTER_GTIDS = ")?;
6571                value.render(ctx, f)
6572            }
6573            Self::SqlAfterMtsGaps { .. } => f.write_str("SQL_AFTER_MTS_GAPS"),
6574        }
6575    }
6576}
6577
6578impl Render for GroupReplicationOption {
6579    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6580        match self {
6581            Self::User { value, .. } => {
6582                f.write_str("USER = ")?;
6583                value.render(ctx, f)
6584            }
6585            Self::Password { value, .. } => {
6586                f.write_str("PASSWORD = ")?;
6587                value.render(ctx, f)
6588            }
6589            Self::DefaultAuth { value, .. } => {
6590                f.write_str("DEFAULT_AUTH = ")?;
6591                value.render(ctx, f)
6592            }
6593        }
6594    }
6595}
6596
6597impl Render for AccountName {
6598    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6599        match self {
6600            Self::Account { user, host, .. } => {
6601                user.render(ctx, f)?;
6602                if let Some(host) = host {
6603                    f.write_str("@")?;
6604                    host.render(ctx, f)?;
6605                }
6606                Ok(())
6607            }
6608            Self::CurrentUser { parens, .. } => {
6609                f.write_str("CURRENT_USER")?;
6610                if *parens {
6611                    f.write_str("()")?;
6612                }
6613                Ok(())
6614            }
6615        }
6616    }
6617}
6618
6619// --- User / role administration DDL render ---------------------------------------------
6620
6621/// The shared `[REQUIRE …] [WITH <resource> …] [<lock option> …] [<attribute>]` option tail of
6622/// `CREATE USER` and `ALTER USER … <list>` — a borrowing view over the four clauses, grouped so
6623/// [`render_user_option_tail`] takes one data parameter rather than four.
6624struct UserOptionTail<'a> {
6625    require: &'a Option<TlsRequirement>,
6626    resource_options: &'a [ResourceLimit],
6627    password_lock_options: &'a [PasswordLockOption],
6628    attribute: &'a Option<UserAttribute>,
6629}
6630
6631/// Render the shared option tail, each clause prefixed by one leading space so it abuts the
6632/// preceding account list.
6633fn render_user_option_tail(
6634    tail: UserOptionTail<'_>,
6635    ctx: &RenderCtx<'_>,
6636    f: &mut fmt::Formatter<'_>,
6637) -> fmt::Result {
6638    if let Some(require) = tail.require {
6639        f.write_str(" ")?;
6640        require.render(ctx, f)?;
6641    }
6642    if !tail.resource_options.is_empty() {
6643        // `WITH` is written once, ahead of a whitespace-separated option run.
6644        f.write_str(" WITH")?;
6645        for option in tail.resource_options {
6646            f.write_str(" ")?;
6647            option.render(ctx, f)?;
6648        }
6649    }
6650    for option in tail.password_lock_options {
6651        f.write_str(" ")?;
6652        option.render(ctx, f)?;
6653    }
6654    if let Some(attribute) = tail.attribute {
6655        f.write_str(" ")?;
6656        attribute.render(ctx, f)?;
6657    }
6658    Ok(())
6659}
6660
6661impl Render for CreateUser {
6662    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6663        f.write_str("CREATE USER ")?;
6664        if self.if_not_exists {
6665            f.write_str("IF NOT EXISTS ")?;
6666        }
6667        render_comma_separated(&self.users, ctx, f)?;
6668        if !self.default_roles.is_empty() {
6669            f.write_str(" DEFAULT ROLE ")?;
6670            render_comma_separated(&self.default_roles, ctx, f)?;
6671        }
6672        render_user_option_tail(
6673            UserOptionTail {
6674                require: &self.require,
6675                resource_options: &self.resource_options,
6676                password_lock_options: &self.password_lock_options,
6677                attribute: &self.attribute,
6678            },
6679            ctx,
6680            f,
6681        )
6682    }
6683}
6684
6685impl Render for UserSpec {
6686    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6687        self.account.render(ctx, f)?;
6688        if let Some(auth) = &self.auth {
6689            f.write_str(" ")?;
6690            auth.render(ctx, f)?;
6691        }
6692        Ok(())
6693    }
6694}
6695
6696impl Render for AuthOption {
6697    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6698        match self {
6699            Self::Password { password, .. } => {
6700                f.write_str("IDENTIFIED BY ")?;
6701                password.render(ctx, f)
6702            }
6703            Self::RandomPassword { .. } => f.write_str("IDENTIFIED BY RANDOM PASSWORD"),
6704            Self::Plugin { plugin, .. } => {
6705                f.write_str("IDENTIFIED WITH ")?;
6706                plugin.render(ctx, f)
6707            }
6708            Self::PluginAs {
6709                plugin,
6710                auth_string,
6711                ..
6712            } => {
6713                f.write_str("IDENTIFIED WITH ")?;
6714                plugin.render(ctx, f)?;
6715                f.write_str(" AS ")?;
6716                auth_string.render(ctx, f)
6717            }
6718            Self::PluginByPassword {
6719                plugin, password, ..
6720            } => {
6721                f.write_str("IDENTIFIED WITH ")?;
6722                plugin.render(ctx, f)?;
6723                f.write_str(" BY ")?;
6724                password.render(ctx, f)
6725            }
6726            Self::PluginByRandomPassword { plugin, .. } => {
6727                f.write_str("IDENTIFIED WITH ")?;
6728                plugin.render(ctx, f)?;
6729                f.write_str(" BY RANDOM PASSWORD")
6730            }
6731        }
6732    }
6733}
6734
6735impl Render for TlsRequirement {
6736    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6737        f.write_str("REQUIRE ")?;
6738        match self {
6739            Self::None { .. } => f.write_str("NONE"),
6740            Self::Ssl { .. } => f.write_str("SSL"),
6741            Self::X509 { .. } => f.write_str("X509"),
6742            Self::Options { options, .. } => {
6743                for (i, option) in options.iter().enumerate() {
6744                    if i > 0 {
6745                        f.write_str(" AND ")?;
6746                    }
6747                    option.render(ctx, f)?;
6748                }
6749                Ok(())
6750            }
6751        }
6752    }
6753}
6754
6755impl Render for TlsOption {
6756    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6757        match self {
6758            Self::Subject { value, .. } => {
6759                f.write_str("SUBJECT ")?;
6760                value.render(ctx, f)
6761            }
6762            Self::Issuer { value, .. } => {
6763                f.write_str("ISSUER ")?;
6764                value.render(ctx, f)
6765            }
6766            Self::Cipher { value, .. } => {
6767                f.write_str("CIPHER ")?;
6768                value.render(ctx, f)
6769            }
6770        }
6771    }
6772}
6773
6774impl Render for ResourceLimit {
6775    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6776        let (keyword, value) = match self {
6777            Self::MaxQueriesPerHour { value, .. } => ("MAX_QUERIES_PER_HOUR", value),
6778            Self::MaxUpdatesPerHour { value, .. } => ("MAX_UPDATES_PER_HOUR", value),
6779            Self::MaxConnectionsPerHour { value, .. } => ("MAX_CONNECTIONS_PER_HOUR", value),
6780            Self::MaxUserConnections { value, .. } => ("MAX_USER_CONNECTIONS", value),
6781        };
6782        f.write_str(keyword)?;
6783        f.write_str(" ")?;
6784        value.render(ctx, f)
6785    }
6786}
6787
6788impl Render for PasswordLockOption {
6789    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6790        match self {
6791            Self::AccountLock { .. } => f.write_str("ACCOUNT LOCK"),
6792            Self::AccountUnlock { .. } => f.write_str("ACCOUNT UNLOCK"),
6793            Self::PasswordExpire { .. } => f.write_str("PASSWORD EXPIRE"),
6794            Self::PasswordExpireDefault { .. } => f.write_str("PASSWORD EXPIRE DEFAULT"),
6795            Self::PasswordExpireNever { .. } => f.write_str("PASSWORD EXPIRE NEVER"),
6796            Self::PasswordExpireInterval { days, .. } => {
6797                f.write_str("PASSWORD EXPIRE INTERVAL ")?;
6798                days.render(ctx, f)?;
6799                f.write_str(" DAY")
6800            }
6801            Self::PasswordHistory { count, .. } => {
6802                f.write_str("PASSWORD HISTORY ")?;
6803                count.render(ctx, f)
6804            }
6805            Self::PasswordHistoryDefault { .. } => f.write_str("PASSWORD HISTORY DEFAULT"),
6806            Self::PasswordReuseInterval { days, .. } => {
6807                f.write_str("PASSWORD REUSE INTERVAL ")?;
6808                days.render(ctx, f)?;
6809                f.write_str(" DAY")
6810            }
6811            Self::PasswordReuseIntervalDefault { .. } => {
6812                f.write_str("PASSWORD REUSE INTERVAL DEFAULT")
6813            }
6814            Self::PasswordRequireCurrent { .. } => f.write_str("PASSWORD REQUIRE CURRENT"),
6815            Self::PasswordRequireCurrentDefault { .. } => {
6816                f.write_str("PASSWORD REQUIRE CURRENT DEFAULT")
6817            }
6818            Self::PasswordRequireCurrentOptional { .. } => {
6819                f.write_str("PASSWORD REQUIRE CURRENT OPTIONAL")
6820            }
6821            Self::FailedLoginAttempts { count, .. } => {
6822                f.write_str("FAILED_LOGIN_ATTEMPTS ")?;
6823                count.render(ctx, f)
6824            }
6825            Self::PasswordLockTime { days, .. } => {
6826                f.write_str("PASSWORD_LOCK_TIME ")?;
6827                days.render(ctx, f)
6828            }
6829            Self::PasswordLockTimeUnbounded { .. } => f.write_str("PASSWORD_LOCK_TIME UNBOUNDED"),
6830        }
6831    }
6832}
6833
6834impl Render for UserAttribute {
6835    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6836        match self {
6837            Self::Comment { comment, .. } => {
6838                f.write_str("COMMENT ")?;
6839                comment.render(ctx, f)
6840            }
6841            Self::Attribute { attribute, .. } => {
6842                f.write_str("ATTRIBUTE ")?;
6843                attribute.render(ctx, f)
6844            }
6845        }
6846    }
6847}
6848
6849impl Render for AlterUser {
6850    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6851        f.write_str("ALTER USER ")?;
6852        match self {
6853            Self::Modify {
6854                if_exists,
6855                users,
6856                require,
6857                resource_options,
6858                password_lock_options,
6859                attribute,
6860                ..
6861            } => {
6862                if *if_exists {
6863                    f.write_str("IF EXISTS ")?;
6864                }
6865                render_comma_separated(users, ctx, f)?;
6866                render_user_option_tail(
6867                    UserOptionTail {
6868                        require,
6869                        resource_options,
6870                        password_lock_options,
6871                        attribute,
6872                    },
6873                    ctx,
6874                    f,
6875                )
6876            }
6877            Self::DefaultRole {
6878                if_exists,
6879                user,
6880                roles,
6881                ..
6882            } => {
6883                if *if_exists {
6884                    f.write_str("IF EXISTS ")?;
6885                }
6886                user.render(ctx, f)?;
6887                f.write_str(" DEFAULT ROLE ")?;
6888                roles.render(ctx, f)
6889            }
6890        }
6891    }
6892}
6893
6894impl Render for AlterUserSpec {
6895    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6896        self.account.render(ctx, f)?;
6897        if let Some(auth) = &self.auth {
6898            f.write_str(" ")?;
6899            auth.render(ctx, f)?;
6900        }
6901        if let Some(replace) = &self.replace {
6902            f.write_str(" REPLACE ")?;
6903            replace.render(ctx, f)?;
6904        }
6905        if self.retain_current_password {
6906            f.write_str(" RETAIN CURRENT PASSWORD")?;
6907        }
6908        if self.discard_old_password {
6909            f.write_str(" DISCARD OLD PASSWORD")?;
6910        }
6911        Ok(())
6912    }
6913}
6914
6915impl Render for DefaultRoleTarget {
6916    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6917        match self {
6918            Self::All { .. } => f.write_str("ALL"),
6919            Self::None { .. } => f.write_str("NONE"),
6920            Self::Roles { roles, .. } => render_comma_separated(roles, ctx, f),
6921        }
6922    }
6923}
6924
6925impl Render for UserRoleList {
6926    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6927        let (verb, guard) = match self.kind {
6928            UserRoleListKind::DropUser => ("DROP USER ", "IF EXISTS "),
6929            UserRoleListKind::CreateRole => ("CREATE ROLE ", "IF NOT EXISTS "),
6930            UserRoleListKind::DropRole => ("DROP ROLE ", "IF EXISTS "),
6931        };
6932        f.write_str(verb)?;
6933        if self.if_guard {
6934            f.write_str(guard)?;
6935        }
6936        render_comma_separated(&self.names, ctx, f)
6937    }
6938}
6939
6940/// The surface keyword for a [`NoWriteToBinlog`] binlog-suppression prefix.
6941fn no_write_to_binlog_keyword(prefix: NoWriteToBinlog) -> &'static str {
6942    match prefix {
6943        NoWriteToBinlog::NoWriteToBinlog => "NO_WRITE_TO_BINLOG",
6944        NoWriteToBinlog::Local => "LOCAL",
6945    }
6946}
6947
6948/// The surface keyword for a [`TableKeyword`] (`TABLE`/`TABLES`).
6949fn table_keyword_str(keyword: TableKeyword) -> &'static str {
6950    match keyword {
6951        TableKeyword::Table => "TABLE",
6952        TableKeyword::Tables => "TABLES",
6953    }
6954}
6955
6956/// The surface keyword(s) for a [`CheckTableOption`].
6957fn check_table_option_keyword(option: CheckTableOption) -> &'static str {
6958    match option {
6959        CheckTableOption::ForUpgrade => "FOR UPGRADE",
6960        CheckTableOption::Quick => "QUICK",
6961        CheckTableOption::Fast => "FAST",
6962        CheckTableOption::Medium => "MEDIUM",
6963        CheckTableOption::Extended => "EXTENDED",
6964        CheckTableOption::Changed => "CHANGED",
6965    }
6966}
6967
6968/// The surface keyword for a [`ChecksumTableOption`].
6969fn checksum_table_option_keyword(option: ChecksumTableOption) -> &'static str {
6970    match option {
6971        ChecksumTableOption::Quick => "QUICK",
6972        ChecksumTableOption::Extended => "EXTENDED",
6973    }
6974}
6975
6976/// The surface keyword for a [`RepairTableOption`].
6977fn repair_table_option_keyword(option: RepairTableOption) -> &'static str {
6978    match option {
6979        RepairTableOption::Quick => "QUICK",
6980        RepairTableOption::Extended => "EXTENDED",
6981        RepairTableOption::UseFrm => "USE_FRM",
6982    }
6983}
6984
6985impl<X: Extension + Render> Render for PrepareStatement<X> {
6986    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6987        f.write_str("PREPARE ")?;
6988        self.name.render(ctx, f)?;
6989        // An empty `parameter_types` is the bare `PREPARE name AS ...` form (no list);
6990        // PostgreSQL rejects an empty written `()`, so a non-empty list unambiguously
6991        // means the parentheses were written.
6992        if !self.parameter_types.is_empty() {
6993            f.write_str("(")?;
6994            render_comma_separated(&self.parameter_types, ctx, f)?;
6995            f.write_str(")")?;
6996        }
6997        f.write_str(" AS ")?;
6998        self.statement.render(ctx, f)
6999    }
7000}
7001
7002impl<X: Extension + Render> Render for ExecuteStatement<X> {
7003    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7004        f.write_str("EXECUTE ")?;
7005        self.name.render(ctx, f)?;
7006        // An empty `args` is the bare `EXECUTE v1` form (no list); a non-empty one renders
7007        // the parenthesized arguments. An empty written `()` is never built (parser-rejected).
7008        if !self.args.is_empty() {
7009            f.write_str("(")?;
7010            render_comma_separated(&self.args, ctx, f)?;
7011            f.write_str(")")?;
7012        }
7013        Ok(())
7014    }
7015}
7016
7017impl Render for DeallocateStatement {
7018    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7019        // The leading verb round-trips MySQL's `deallocate_or_drop` synonym choice; DuckDB
7020        // only ever spells `DEALLOCATE`.
7021        f.write_str(match self.keyword {
7022            DeallocateKeyword::Deallocate => "DEALLOCATE ",
7023            DeallocateKeyword::Drop => "DROP ",
7024        })?;
7025        if self.prepare_keyword {
7026            f.write_str("PREPARE ")?;
7027        }
7028        self.name.render(ctx, f)
7029    }
7030}
7031
7032impl Render for PrepareFromStatement {
7033    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7034        f.write_str("PREPARE ")?;
7035        self.name.render(ctx, f)?;
7036        f.write_str(" FROM ")?;
7037        self.source.render(ctx, f)
7038    }
7039}
7040
7041impl Render for PrepareSource {
7042    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7043        match self {
7044            // The string source round-trips verbatim from the `Literal`.
7045            PrepareSource::Text { source, .. } => source.render(ctx, f),
7046            // A `@variable` source: the `@` sigil plus the name, whose quote style the `Ident`
7047            // carries.
7048            PrepareSource::Variable { name, .. } => {
7049                f.write_str("@")?;
7050                name.render(ctx, f)
7051            }
7052        }
7053    }
7054}
7055
7056impl Render for ExecuteUsingStatement {
7057    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7058        f.write_str("EXECUTE ")?;
7059        self.name.render(ctx, f)?;
7060        // A non-empty list renders the `USING` clause; an empty one is the bare `EXECUTE name`
7061        // form (no `USING` written — MySQL has no empty-`USING` spelling).
7062        if let Some((first, rest)) = self.using.split_first() {
7063            f.write_str(" USING @")?;
7064            first.render(ctx, f)?;
7065            for name in rest {
7066                f.write_str(", @")?;
7067                name.render(ctx, f)?;
7068            }
7069        }
7070        Ok(())
7071    }
7072}
7073
7074impl<X: Extension + Render> Render for CallStatement<X> {
7075    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7076        f.write_str("CALL ")?;
7077        self.name.render(ctx, f)?;
7078        // The parenthesized argument list is a surface flag: the DuckDB form always writes
7079        // it (empty or not), MySQL's bare `CALL name` writes no list at all.
7080        if self.parenthesized {
7081            f.write_str("(")?;
7082            render_comma_separated(&self.args, ctx, f)?;
7083            f.write_str(")")?;
7084        }
7085        Ok(())
7086    }
7087}
7088
7089impl Render for DoStatement {
7090    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7091        f.write_str("DO")?;
7092        // The list is always non-empty (the parser rejects a bare `DO`); each item follows a
7093        // single space, and the source order round-trips (a body and a language clause can
7094        // appear in either order, and either may repeat).
7095        for arg in &self.args {
7096            f.write_str(" ")?;
7097            arg.render(ctx, f)?;
7098        }
7099        Ok(())
7100    }
7101}
7102
7103impl Render for DoArg {
7104    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7105        match self {
7106            DoArg::Body { body, .. } => body.render(ctx, f),
7107            DoArg::Language { name, .. } => {
7108                f.write_str("LANGUAGE ")?;
7109                name.render(ctx, f)
7110            }
7111        }
7112    }
7113}
7114
7115impl Render for LanguageName {
7116    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7117        match self {
7118            Self::Word { word, .. } => word.render(ctx, f),
7119            Self::String { value, .. } => value.render(ctx, f),
7120        }
7121    }
7122}
7123
7124impl<X: Extension + Render> Render for DoExpressionsStatement<X> {
7125    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7126        // The list is always non-empty (the parser rejects a bare `DO`).
7127        f.write_str("DO ")?;
7128        render_comma_separated(&self.items, ctx, f)
7129    }
7130}
7131
7132impl Render for LockTablesStatement {
7133    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7134        // `plural` preserves the interchangeable `TABLES`/`TABLE` source spelling; the
7135        // list is always non-empty (the parser rejects a bare `LOCK TABLES`).
7136        f.write_str(if self.plural {
7137            "LOCK TABLES "
7138        } else {
7139            "LOCK TABLE "
7140        })?;
7141        render_comma_separated(&self.tables, ctx, f)
7142    }
7143}
7144
7145impl Render for TableLock {
7146    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7147        self.name.render(ctx, f)?;
7148        if let Some(alias) = &self.alias {
7149            // The canonical `AS`-less spelling: MySQL's `opt_as` makes the keyword pure
7150            // noise the AST does not record.
7151            f.write_str(" ")?;
7152            alias.render(ctx, f)?;
7153        }
7154        f.write_str(" ")?;
7155        self.kind.render(ctx, f)
7156    }
7157}
7158
7159impl Render for TableLockKind {
7160    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7161        f.write_str(match self {
7162            TableLockKind::Read => "READ",
7163            TableLockKind::ReadLocal => "READ LOCAL",
7164            TableLockKind::Write => "WRITE",
7165        })
7166    }
7167}
7168
7169impl Render for UnlockTablesStatement {
7170    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7171        f.write_str(if self.plural {
7172            "UNLOCK TABLES"
7173        } else {
7174            "UNLOCK TABLE"
7175        })
7176    }
7177}
7178
7179impl Render for InstanceLockStatement {
7180    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7181        f.write_str(if self.acquire {
7182            "LOCK INSTANCE FOR BACKUP"
7183        } else {
7184            "UNLOCK INSTANCE"
7185        })
7186    }
7187}
7188
7189impl<X: Extension + Render> Render for LoadDataStatement<X> {
7190    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7191        // The grammar's canonical clause order — the parse layer is order-sensitive, so the
7192        // rendered order is the only round-trippable one.
7193        f.write_str("LOAD ")?;
7194        self.format.render(ctx, f)?;
7195        if let Some(concurrency) = self.concurrency {
7196            f.write_str(" ")?;
7197            concurrency.render(ctx, f)?;
7198        }
7199        if self.local {
7200            f.write_str(" LOCAL")?;
7201        }
7202        f.write_str(" INFILE ")?;
7203        self.file.render(ctx, f)?;
7204        if let Some(on_duplicate) = self.on_duplicate {
7205            f.write_str(" ")?;
7206            on_duplicate.render(ctx, f)?;
7207        }
7208        f.write_str(" INTO TABLE ")?;
7209        self.table.render(ctx, f)?;
7210        if !self.partitions.is_empty() {
7211            f.write_str(" PARTITION (")?;
7212            render_comma_separated(&self.partitions, ctx, f)?;
7213            f.write_str(")")?;
7214        }
7215        if let Some(charset) = &self.charset {
7216            f.write_str(" CHARACTER SET ")?;
7217            charset.render(ctx, f)?;
7218        }
7219        if let Some(tag) = &self.rows_identified_by {
7220            f.write_str(" ROWS IDENTIFIED BY ")?;
7221            tag.render(ctx, f)?;
7222        }
7223        if let Some(fields) = &self.fields {
7224            f.write_str(" ")?;
7225            fields.render(ctx, f)?;
7226        }
7227        if let Some(lines) = &self.lines {
7228            f.write_str(" ")?;
7229            lines.render(ctx, f)?;
7230        }
7231        if let Some(ignore_rows) = &self.ignore_rows {
7232            f.write_str(" ")?;
7233            ignore_rows.render(ctx, f)?;
7234        }
7235        if !self.columns.is_empty() {
7236            f.write_str(" (")?;
7237            render_comma_separated(&self.columns, ctx, f)?;
7238            f.write_str(")")?;
7239        }
7240        if !self.set.is_empty() {
7241            f.write_str(" SET ")?;
7242            render_comma_separated(&self.set, ctx, f)?;
7243        }
7244        Ok(())
7245    }
7246}
7247
7248impl Render for LoadDataFormat {
7249    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7250        f.write_str(match self {
7251            LoadDataFormat::Data => "DATA",
7252            LoadDataFormat::Xml => "XML",
7253        })
7254    }
7255}
7256
7257impl Render for LoadDataConcurrency {
7258    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7259        f.write_str(match self {
7260            LoadDataConcurrency::LowPriority => "LOW_PRIORITY",
7261            LoadDataConcurrency::Concurrent => "CONCURRENT",
7262        })
7263    }
7264}
7265
7266impl Render for LoadDataDuplicate {
7267    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7268        f.write_str(match self {
7269            LoadDataDuplicate::Replace => "REPLACE",
7270            LoadDataDuplicate::Ignore => "IGNORE",
7271        })
7272    }
7273}
7274
7275impl Render for LoadDataFields {
7276    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7277        // The sub-clauses render in the grammar's canonical order regardless of source order
7278        // (any order re-parses to the same node); at least one is always present.
7279        self.spelling.render(ctx, f)?;
7280        if let Some(terminated_by) = &self.terminated_by {
7281            f.write_str(" TERMINATED BY ")?;
7282            terminated_by.render(ctx, f)?;
7283        }
7284        if let Some(enclosed_by) = &self.enclosed_by {
7285            f.write_str(" ")?;
7286            enclosed_by.render(ctx, f)?;
7287        }
7288        if let Some(escaped_by) = &self.escaped_by {
7289            f.write_str(" ESCAPED BY ")?;
7290            escaped_by.render(ctx, f)?;
7291        }
7292        Ok(())
7293    }
7294}
7295
7296impl Render for LoadFieldsSpelling {
7297    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7298        f.write_str(match self {
7299            LoadFieldsSpelling::Fields => "FIELDS",
7300            LoadFieldsSpelling::Columns => "COLUMNS",
7301        })
7302    }
7303}
7304
7305impl Render for LoadDataEnclosed {
7306    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7307        if self.optionally {
7308            f.write_str("OPTIONALLY ")?;
7309        }
7310        f.write_str("ENCLOSED BY ")?;
7311        self.value.render(ctx, f)
7312    }
7313}
7314
7315impl Render for LoadDataLines {
7316    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7317        f.write_str("LINES")?;
7318        if let Some(starting_by) = &self.starting_by {
7319            f.write_str(" STARTING BY ")?;
7320            starting_by.render(ctx, f)?;
7321        }
7322        if let Some(terminated_by) = &self.terminated_by {
7323            f.write_str(" TERMINATED BY ")?;
7324            terminated_by.render(ctx, f)?;
7325        }
7326        Ok(())
7327    }
7328}
7329
7330impl Render for LoadDataIgnoreRows {
7331    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7332        f.write_str("IGNORE ")?;
7333        self.count.render(ctx, f)?;
7334        f.write_str(" ")?;
7335        self.unit.render(ctx, f)
7336    }
7337}
7338
7339impl Render for LoadDataIgnoreUnit {
7340    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7341        f.write_str(match self {
7342            LoadDataIgnoreUnit::Lines => "LINES",
7343            LoadDataIgnoreUnit::Rows => "ROWS",
7344        })
7345    }
7346}
7347
7348impl Render for LoadDataFieldOrVar {
7349    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7350        match self {
7351            LoadDataFieldOrVar::Column { name, .. } => name.render(ctx, f),
7352            // A `@variable` target: the `@` sigil plus the name (whose quote style the `Ident`
7353            // carries).
7354            LoadDataFieldOrVar::Variable { name, .. } => {
7355                f.write_str("@")?;
7356                name.render(ctx, f)
7357            }
7358        }
7359    }
7360}
7361
7362impl<X: Extension + Render> Render for CreateTrigger<X> {
7363    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7364        f.write_str("CREATE")?;
7365        if let Some(temporary) = self.temporary {
7366            f.write_str(" ")?;
7367            temporary.render(ctx, f)?;
7368        }
7369        f.write_str(" TRIGGER")?;
7370        if self.if_not_exists {
7371            f.write_str(" IF NOT EXISTS")?;
7372        }
7373        f.write_str(" ")?;
7374        self.name.render(ctx, f)?;
7375        if let Some(timing) = self.timing {
7376            f.write_str(" ")?;
7377            timing.render(ctx, f)?;
7378        }
7379        f.write_str(" ")?;
7380        self.event.render(ctx, f)?;
7381        f.write_str(" ON ")?;
7382        self.table.render(ctx, f)?;
7383        if self.for_each_row {
7384            f.write_str(" FOR EACH ROW")?;
7385        }
7386        if let Some(when) = &self.when {
7387            f.write_str(" WHEN ")?;
7388            when.render(ctx, f)?;
7389        }
7390        f.write_str(" BEGIN ")?;
7391        for statement in &self.body {
7392            statement.render(ctx, f)?;
7393            f.write_str("; ")?;
7394        }
7395        f.write_str("END")
7396    }
7397}
7398
7399impl Render for TriggerTiming {
7400    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7401        f.write_str(match self {
7402            TriggerTiming::Before => "BEFORE",
7403            TriggerTiming::After => "AFTER",
7404            TriggerTiming::InsteadOf => "INSTEAD OF",
7405        })
7406    }
7407}
7408
7409impl Render for TriggerEvent {
7410    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7411        match self {
7412            TriggerEvent::Delete { .. } => f.write_str("DELETE"),
7413            TriggerEvent::Insert { .. } => f.write_str("INSERT"),
7414            TriggerEvent::Update { columns, .. } => {
7415                f.write_str("UPDATE")?;
7416                if !columns.is_empty() {
7417                    f.write_str(" OF ")?;
7418                    render_ident_list(columns, ctx, f)?;
7419                }
7420                Ok(())
7421            }
7422        }
7423    }
7424}
7425
7426impl<X: Extension + Render> Render for CreateStoredTrigger<X> {
7427    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7428        f.write_str("CREATE ")?;
7429        if let Some(definer) = &self.definer {
7430            definer.render(ctx, f)?;
7431            f.write_str(" ")?;
7432        }
7433        f.write_str("TRIGGER ")?;
7434        if self.if_not_exists {
7435            f.write_str("IF NOT EXISTS ")?;
7436        }
7437        self.name.render(ctx, f)?;
7438        f.write_str(" ")?;
7439        self.timing.render(ctx, f)?;
7440        f.write_str(" ")?;
7441        self.event.render(ctx, f)?;
7442        f.write_str(" ON ")?;
7443        self.table.render(ctx, f)?;
7444        f.write_str(" FOR EACH ROW")?;
7445        if let Some(ordering) = &self.ordering {
7446            f.write_str(" ")?;
7447            ordering.render(ctx, f)?;
7448        }
7449        f.write_str(" ")?;
7450        self.body.render(ctx, f)
7451    }
7452}
7453
7454impl Render for TriggerOrder {
7455    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7456        match self {
7457            TriggerOrder::Follows { anchor, .. } => {
7458                f.write_str("FOLLOWS ")?;
7459                anchor.render(ctx, f)
7460            }
7461            TriggerOrder::Precedes { anchor, .. } => {
7462                f.write_str("PRECEDES ")?;
7463                anchor.render(ctx, f)
7464            }
7465        }
7466    }
7467}
7468
7469// ---------------------------------------------------------------------------
7470// Stored-program compound statements (MySQL SQL/PSM)
7471// ---------------------------------------------------------------------------
7472
7473/// Render a `;`-terminated compound-body statement list, each element prefixed by a
7474/// single leading space (` <stmt>;`) — the shared shape of every block, branch, and
7475/// loop body, mirroring the trigger body's `<stmt>; ` join.
7476fn render_compound_body<X: Extension + Render>(
7477    body: &[Statement<X>],
7478    ctx: &RenderCtx<'_>,
7479    f: &mut fmt::Formatter<'_>,
7480) -> fmt::Result {
7481    for statement in body {
7482        f.write_str(" ")?;
7483        statement.render(ctx, f)?;
7484        f.write_str(";")?;
7485    }
7486    Ok(())
7487}
7488
7489impl<X: Extension + Render> Render for CompoundStatement<X> {
7490    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7491        if let Some(label) = &self.label {
7492            label.render(ctx, f)?;
7493            f.write_str(": ")?;
7494        }
7495        f.write_str("BEGIN")?;
7496        for declaration in &self.declarations {
7497            f.write_str(" ")?;
7498            declaration.render(ctx, f)?;
7499            f.write_str(";")?;
7500        }
7501        render_compound_body(&self.body, ctx, f)?;
7502        f.write_str(" END")?;
7503        if let Some(end_label) = &self.end_label {
7504            f.write_str(" ")?;
7505            end_label.render(ctx, f)?;
7506        }
7507        Ok(())
7508    }
7509}
7510
7511impl<X: Extension + Render> Render for Declaration<X> {
7512    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7513        match self {
7514            Declaration::Variable {
7515                names,
7516                data_type,
7517                default,
7518                ..
7519            } => {
7520                f.write_str("DECLARE ")?;
7521                render_ident_list(names, ctx, f)?;
7522                f.write_str(" ")?;
7523                data_type.render(ctx, f)?;
7524                if let Some(default) = default {
7525                    f.write_str(" DEFAULT ")?;
7526                    default.render(ctx, f)?;
7527                }
7528                Ok(())
7529            }
7530            Declaration::Condition { name, value, .. } => {
7531                f.write_str("DECLARE ")?;
7532                name.render(ctx, f)?;
7533                f.write_str(" CONDITION FOR ")?;
7534                value.render(ctx, f)
7535            }
7536            Declaration::Cursor { name, query, .. } => {
7537                f.write_str("DECLARE ")?;
7538                name.render(ctx, f)?;
7539                f.write_str(" CURSOR FOR ")?;
7540                query.render(ctx, f)
7541            }
7542            Declaration::Handler {
7543                action,
7544                conditions,
7545                body,
7546                ..
7547            } => {
7548                f.write_str("DECLARE ")?;
7549                action.render(ctx, f)?;
7550                f.write_str(" HANDLER FOR ")?;
7551                render_comma_separated(conditions, ctx, f)?;
7552                f.write_str(" ")?;
7553                body.render(ctx, f)
7554            }
7555        }
7556    }
7557}
7558
7559impl Render for HandlerAction {
7560    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7561        f.write_str(match self {
7562            HandlerAction::Continue => "CONTINUE",
7563            HandlerAction::Exit => "EXIT",
7564            HandlerAction::Undo => "UNDO",
7565        })
7566    }
7567}
7568
7569impl Render for ConditionValue {
7570    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7571        match self {
7572            ConditionValue::SqlState {
7573                value_keyword,
7574                sqlstate,
7575                ..
7576            } => {
7577                f.write_str("SQLSTATE ")?;
7578                if *value_keyword {
7579                    f.write_str("VALUE ")?;
7580                }
7581                sqlstate.render(ctx, f)
7582            }
7583            ConditionValue::ErrorCode { code, .. } => code.render(ctx, f),
7584            ConditionValue::ConditionName { name, .. } => name.render(ctx, f),
7585        }
7586    }
7587}
7588
7589impl Render for HandlerCondition {
7590    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7591        match self {
7592            HandlerCondition::SqlState {
7593                value_keyword,
7594                sqlstate,
7595                ..
7596            } => {
7597                f.write_str("SQLSTATE ")?;
7598                if *value_keyword {
7599                    f.write_str("VALUE ")?;
7600                }
7601                sqlstate.render(ctx, f)
7602            }
7603            HandlerCondition::ErrorCode { code, .. } => code.render(ctx, f),
7604            HandlerCondition::ConditionName { name, .. } => name.render(ctx, f),
7605            HandlerCondition::SqlWarning { .. } => f.write_str("SQLWARNING"),
7606            HandlerCondition::NotFound { .. } => f.write_str("NOT FOUND"),
7607            HandlerCondition::SqlException { .. } => f.write_str("SQLEXCEPTION"),
7608        }
7609    }
7610}
7611
7612impl<X: Extension + Render> Render for ConditionalBranch<X> {
7613    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7614        self.guard.render(ctx, f)?;
7615        f.write_str(" THEN")?;
7616        render_compound_body(&self.body, ctx, f)
7617    }
7618}
7619
7620impl<X: Extension + Render> Render for IfStatement<X> {
7621    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7622        f.write_str("IF ")?;
7623        for (i, branch) in self.branches.iter().enumerate() {
7624            if i > 0 {
7625                f.write_str(" ELSEIF ")?;
7626            }
7627            branch.render(ctx, f)?;
7628        }
7629        if let Some(else_body) = &self.else_body {
7630            f.write_str(" ELSE")?;
7631            render_compound_body(else_body, ctx, f)?;
7632        }
7633        f.write_str(" END IF")
7634    }
7635}
7636
7637impl<X: Extension + Render> Render for CaseStatement<X> {
7638    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7639        f.write_str("CASE")?;
7640        if let Some(operand) = &self.operand {
7641            f.write_str(" ")?;
7642            operand.render(ctx, f)?;
7643        }
7644        for branch in &self.when_branches {
7645            f.write_str(" WHEN ")?;
7646            branch.render(ctx, f)?;
7647        }
7648        if let Some(else_body) = &self.else_body {
7649            f.write_str(" ELSE")?;
7650            render_compound_body(else_body, ctx, f)?;
7651        }
7652        f.write_str(" END CASE")
7653    }
7654}
7655
7656impl<X: Extension + Render> Render for LoopStatement<X> {
7657    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7658        if let Some(label) = &self.label {
7659            label.render(ctx, f)?;
7660            f.write_str(": ")?;
7661        }
7662        f.write_str("LOOP")?;
7663        render_compound_body(&self.body, ctx, f)?;
7664        f.write_str(" END LOOP")?;
7665        if let Some(end_label) = &self.end_label {
7666            f.write_str(" ")?;
7667            end_label.render(ctx, f)?;
7668        }
7669        Ok(())
7670    }
7671}
7672
7673impl<X: Extension + Render> Render for WhileStatement<X> {
7674    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7675        if let Some(label) = &self.label {
7676            label.render(ctx, f)?;
7677            f.write_str(": ")?;
7678        }
7679        f.write_str("WHILE ")?;
7680        self.condition.render(ctx, f)?;
7681        f.write_str(" DO")?;
7682        render_compound_body(&self.body, ctx, f)?;
7683        f.write_str(" END WHILE")?;
7684        if let Some(end_label) = &self.end_label {
7685            f.write_str(" ")?;
7686            end_label.render(ctx, f)?;
7687        }
7688        Ok(())
7689    }
7690}
7691
7692impl<X: Extension + Render> Render for RepeatStatement<X> {
7693    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7694        if let Some(label) = &self.label {
7695            label.render(ctx, f)?;
7696            f.write_str(": ")?;
7697        }
7698        f.write_str("REPEAT")?;
7699        render_compound_body(&self.body, ctx, f)?;
7700        f.write_str(" UNTIL ")?;
7701        self.condition.render(ctx, f)?;
7702        f.write_str(" END REPEAT")?;
7703        if let Some(end_label) = &self.end_label {
7704            f.write_str(" ")?;
7705            end_label.render(ctx, f)?;
7706        }
7707        Ok(())
7708    }
7709}
7710
7711impl Render for LeaveStatement {
7712    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7713        f.write_str("LEAVE ")?;
7714        self.label.render(ctx, f)
7715    }
7716}
7717
7718impl Render for IterateStatement {
7719    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7720        f.write_str("ITERATE ")?;
7721        self.label.render(ctx, f)
7722    }
7723}
7724
7725impl<X: Extension + Render> Render for ReturnStatement<X> {
7726    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7727        f.write_str("RETURN ")?;
7728        self.value.render(ctx, f)
7729    }
7730}
7731
7732impl Render for OpenCursorStatement {
7733    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7734        f.write_str("OPEN ")?;
7735        self.cursor.render(ctx, f)
7736    }
7737}
7738
7739impl Render for CloseCursorStatement {
7740    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7741        f.write_str("CLOSE ")?;
7742        self.cursor.render(ctx, f)
7743    }
7744}
7745
7746impl Render for FetchCursorStatement {
7747    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7748        f.write_str("FETCH ")?;
7749        if self.next_keyword {
7750            f.write_str("NEXT ")?;
7751        }
7752        if self.from_keyword {
7753            f.write_str("FROM ")?;
7754        }
7755        self.cursor.render(ctx, f)?;
7756        f.write_str(" INTO ")?;
7757        render_ident_list(&self.targets, ctx, f)
7758    }
7759}
7760
7761impl<X: Extension + Render> SignalStatement<X> {
7762    /// Render as `SIGNAL`/`RESIGNAL` — the two share this payload and differ only in the
7763    /// leading keyword the caller supplies.
7764    fn render_as(
7765        &self,
7766        ctx: &RenderCtx<'_>,
7767        f: &mut fmt::Formatter<'_>,
7768        keyword: &str,
7769    ) -> fmt::Result {
7770        f.write_str(keyword)?;
7771        if let Some(condition) = &self.condition {
7772            f.write_str(" ")?;
7773            condition.render(ctx, f)?;
7774        }
7775        if !self.set_items.is_empty() {
7776            f.write_str(" SET ")?;
7777            render_comma_separated(&self.set_items, ctx, f)?;
7778        }
7779        Ok(())
7780    }
7781}
7782
7783impl<X: Extension + Render> Render for SignalItem<X> {
7784    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7785        self.name.render(ctx, f)?;
7786        f.write_str(" = ")?;
7787        self.value.render(ctx, f)
7788    }
7789}
7790
7791impl Render for SignalItemName {
7792    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7793        f.write_str(match self {
7794            SignalItemName::ClassOrigin => "CLASS_ORIGIN",
7795            SignalItemName::SubclassOrigin => "SUBCLASS_ORIGIN",
7796            SignalItemName::ConstraintCatalog => "CONSTRAINT_CATALOG",
7797            SignalItemName::ConstraintSchema => "CONSTRAINT_SCHEMA",
7798            SignalItemName::ConstraintName => "CONSTRAINT_NAME",
7799            SignalItemName::CatalogName => "CATALOG_NAME",
7800            SignalItemName::SchemaName => "SCHEMA_NAME",
7801            SignalItemName::TableName => "TABLE_NAME",
7802            SignalItemName::ColumnName => "COLUMN_NAME",
7803            SignalItemName::CursorName => "CURSOR_NAME",
7804            SignalItemName::MessageText => "MESSAGE_TEXT",
7805            SignalItemName::MysqlErrno => "MYSQL_ERRNO",
7806        })
7807    }
7808}
7809
7810impl<X: Extension + Render> Render for GetDiagnosticsStatement<X> {
7811    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7812        f.write_str("GET")?;
7813        match self.area {
7814            DiagnosticsArea::Implicit => {}
7815            DiagnosticsArea::Current => f.write_str(" CURRENT")?,
7816            DiagnosticsArea::Stacked => f.write_str(" STACKED")?,
7817        }
7818        f.write_str(" DIAGNOSTICS ")?;
7819        self.info.render(ctx, f)
7820    }
7821}
7822
7823impl<X: Extension + Render> Render for DiagnosticsInfo<X> {
7824    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7825        match self {
7826            DiagnosticsInfo::Statement { items, .. } => render_comma_separated(items, ctx, f),
7827            DiagnosticsInfo::Condition { number, items, .. } => {
7828                f.write_str("CONDITION ")?;
7829                number.render(ctx, f)?;
7830                f.write_str(" ")?;
7831                render_comma_separated(items, ctx, f)
7832            }
7833        }
7834    }
7835}
7836
7837impl<X: Extension + Render> Render for StatementInfoItem<X> {
7838    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7839        self.target.render(ctx, f)?;
7840        f.write_str(" = ")?;
7841        self.name.render(ctx, f)
7842    }
7843}
7844
7845impl Render for StatementInfoItemName {
7846    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7847        f.write_str(match self {
7848            StatementInfoItemName::Number => "NUMBER",
7849            StatementInfoItemName::RowCount => "ROW_COUNT",
7850        })
7851    }
7852}
7853
7854impl<X: Extension + Render> Render for ConditionInfoItem<X> {
7855    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7856        self.target.render(ctx, f)?;
7857        f.write_str(" = ")?;
7858        self.name.render(ctx, f)
7859    }
7860}
7861
7862impl Render for ConditionInfoItemName {
7863    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7864        f.write_str(match self {
7865            ConditionInfoItemName::ClassOrigin => "CLASS_ORIGIN",
7866            ConditionInfoItemName::SubclassOrigin => "SUBCLASS_ORIGIN",
7867            ConditionInfoItemName::ConstraintCatalog => "CONSTRAINT_CATALOG",
7868            ConditionInfoItemName::ConstraintSchema => "CONSTRAINT_SCHEMA",
7869            ConditionInfoItemName::ConstraintName => "CONSTRAINT_NAME",
7870            ConditionInfoItemName::CatalogName => "CATALOG_NAME",
7871            ConditionInfoItemName::SchemaName => "SCHEMA_NAME",
7872            ConditionInfoItemName::TableName => "TABLE_NAME",
7873            ConditionInfoItemName::ColumnName => "COLUMN_NAME",
7874            ConditionInfoItemName::CursorName => "CURSOR_NAME",
7875            ConditionInfoItemName::MessageText => "MESSAGE_TEXT",
7876            ConditionInfoItemName::MysqlErrno => "MYSQL_ERRNO",
7877            ConditionInfoItemName::ReturnedSqlstate => "RETURNED_SQLSTATE",
7878        })
7879    }
7880}
7881
7882impl<X: Extension + Render> Render for CreateTable<X> {
7883    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7884        f.write_str("CREATE")?;
7885        if self.or_replace {
7886            f.write_str(" OR REPLACE")?;
7887        }
7888        if let Some(temporary) = self.temporary {
7889            f.write_str(" ")?;
7890            temporary.render(ctx, f)?;
7891        }
7892        // `UNLOGGED` is a peer of `TEMP`/`TEMPORARY` in PostgreSQL's `OptTemp`, so it never
7893        // co-occurs with `temporary` — exactly one persistence keyword is written.
7894        if self.unlogged {
7895            f.write_str(" UNLOGGED")?;
7896        }
7897        f.write_str(" TABLE")?;
7898        if self.if_not_exists {
7899            f.write_str(" IF NOT EXISTS")?;
7900        }
7901        f.write_str(" ")?;
7902        self.name.render(ctx, f)?;
7903        match &self.body {
7904            // PostgreSQL grammar order after the body: `INHERITS (…)`, then `PARTITION BY …`,
7905            // then `USING <method>`, then the trailing options. `inherits` is only ever non-empty
7906            // on a `Definition` body (never `PartitionOf`/`OfType`), and an `AS <query>` body
7907            // carries none of these, so they are unrendered there.
7908            CreateTableBody::Definition { .. }
7909            | CreateTableBody::PartitionOf { .. }
7910            | CreateTableBody::OfType { .. }
7911            // MySQL's `LIKE <source>` clone body carries no trailing clause of its own (the
7912            // grammar rejects options after it), so `inherits`/`partition_by`/`access_method`/
7913            // `options` are empty/`None` and their renders are no-ops here.
7914            | CreateTableBody::LikeSource { .. } => {
7915                self.body.render(ctx, f)?;
7916                render_inherits(&self.inherits, ctx, f)?;
7917                render_partition_by(&self.partition_by, ctx, f)?;
7918                render_access_method(&self.access_method, ctx, f)?;
7919                render_create_table_options(&self.options, ctx, f)?;
7920            }
7921            CreateTableBody::AsQuery {
7922                columns,
7923                query,
7924                with_data,
7925                ..
7926            } => {
7927                render_ctas_columns(columns, ctx, f)?;
7928                // The CTAS `USING` slot precedes the options and the `AS` (PostgreSQL's
7929                // `CreateTableAsStmt` order, mirroring the tail order above).
7930                render_access_method(&self.access_method, ctx, f)?;
7931                render_create_table_options(&self.options, ctx, f)?;
7932                render_ctas_query_tail(query, *with_data, ctx, f)?;
7933            }
7934            CreateTableBody::AsExecute {
7935                columns,
7936                execute,
7937                with_data,
7938                ..
7939            } => {
7940                render_ctas_columns(columns, ctx, f)?;
7941                render_access_method(&self.access_method, ctx, f)?;
7942                render_create_table_options(&self.options, ctx, f)?;
7943                f.write_str(" AS ")?;
7944                execute.render(ctx, f)?;
7945                render_with_data(*with_data, f)?;
7946            }
7947        }
7948        Ok(())
7949    }
7950}
7951
7952/// Render the ` INHERITS (<parent>, ...)` clause when the parent list is non-empty; the
7953/// leading space is part of the clause separator, and an empty list (no clause) renders nothing.
7954fn render_inherits(
7955    inherits: &[ObjectName],
7956    ctx: &RenderCtx<'_>,
7957    f: &mut fmt::Formatter<'_>,
7958) -> fmt::Result {
7959    if !inherits.is_empty() {
7960        f.write_str(" INHERITS (")?;
7961        render_comma_separated(inherits, ctx, f)?;
7962        f.write_str(")")?;
7963    }
7964    Ok(())
7965}
7966
7967/// Render the trailing ` PARTITION BY {LIST | RANGE | HASH} (…)` clause when present; the
7968/// leading space is part of the clause separator.
7969fn render_partition_by<X: Extension + Render>(
7970    partition_by: &Option<Box<PartitionSpec<X>>>,
7971    ctx: &RenderCtx<'_>,
7972    f: &mut fmt::Formatter<'_>,
7973) -> fmt::Result {
7974    if let Some(spec) = partition_by {
7975        f.write_str(" PARTITION BY ")?;
7976        spec.render(ctx, f)?;
7977    }
7978    Ok(())
7979}
7980
7981/// Render the trailing ` USING <access_method>` clause when present; the leading space is part
7982/// of the clause separator.
7983fn render_access_method(
7984    access_method: &Option<Box<Ident>>,
7985    ctx: &RenderCtx<'_>,
7986    f: &mut fmt::Formatter<'_>,
7987) -> fmt::Result {
7988    if let Some(method) = access_method {
7989        f.write_str(" USING ")?;
7990        method.render(ctx, f)?;
7991    }
7992    Ok(())
7993}
7994
7995impl Render for TemporaryTableKind {
7996    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
7997        f.write_str(match self {
7998            Self::Temp => "TEMP",
7999            Self::Temporary => "TEMPORARY",
8000        })
8001    }
8002}
8003
8004/// Render a `CREATE TABLE`'s trailing options with the dialect-appropriate separator.
8005/// SQLite comma-separates its options (`STRICT, WITHOUT ROWID`); MySQL/PostgreSQL
8006/// space-separate theirs (`ENGINE = InnoDB AUTO_INCREMENT = 100`). The two never mix in
8007/// one statement, so a SQLite keyword-style option after the first carries the comma;
8008/// everything else keeps the plain space, and the first option always leads with a space.
8009fn render_create_table_options<X: Extension + Render>(
8010    options: &[CreateTableOption<X>],
8011    ctx: &RenderCtx<'_>,
8012    f: &mut fmt::Formatter<'_>,
8013) -> fmt::Result {
8014    for (index, option) in options.iter().enumerate() {
8015        let sqlite_option = matches!(
8016            option.kind,
8017            CreateTableOptionKind::WithoutRowid { .. } | CreateTableOptionKind::Strict { .. }
8018        );
8019        f.write_str(if index > 0 && sqlite_option {
8020            ", "
8021        } else {
8022            " "
8023        })?;
8024        option.render(ctx, f)?;
8025    }
8026    Ok(())
8027}
8028
8029impl<X: Extension + Render> Render for CreateTableBody<X> {
8030    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8031        match self {
8032            Self::Definition { elements, .. } => {
8033                f.write_str(" (")?;
8034                render_comma_separated(elements, ctx, f)?;
8035                f.write_str(")")
8036            }
8037            Self::AsQuery {
8038                columns,
8039                query,
8040                with_data,
8041                ..
8042            } => {
8043                render_ctas_columns(columns, ctx, f)?;
8044                render_ctas_query_tail(query, *with_data, ctx, f)
8045            }
8046            Self::AsExecute {
8047                columns,
8048                execute,
8049                with_data,
8050                ..
8051            } => {
8052                render_ctas_columns(columns, ctx, f)?;
8053                f.write_str(" AS ")?;
8054                execute.render(ctx, f)?;
8055                render_with_data(*with_data, f)
8056            }
8057            Self::PartitionOf {
8058                parent,
8059                elements,
8060                bound,
8061                ..
8062            } => {
8063                f.write_str(" PARTITION OF ")?;
8064                parent.render(ctx, f)?;
8065                // The augmentation list is absent (never empty-with-parens: PostgreSQL rejects
8066                // an empty `()`), so an empty vec renders no parentheses.
8067                if !elements.is_empty() {
8068                    f.write_str(" (")?;
8069                    render_comma_separated(elements, ctx, f)?;
8070                    f.write_str(")")?;
8071                }
8072                f.write_str(" ")?;
8073                bound.render(ctx, f)
8074            }
8075            Self::OfType {
8076                type_name,
8077                elements,
8078                ..
8079            } => {
8080                f.write_str(" OF ")?;
8081                type_name.render(ctx, f)?;
8082                // The augmentation list is absent (never empty-with-parens: PostgreSQL rejects an
8083                // empty `()`), so an empty vec renders no parentheses.
8084                if !elements.is_empty() {
8085                    f.write_str(" (")?;
8086                    render_comma_separated(elements, ctx, f)?;
8087                    f.write_str(")")?;
8088                }
8089                Ok(())
8090            }
8091            Self::LikeSource {
8092                source,
8093                parenthesized,
8094                ..
8095            } => {
8096                // MySQL's two spellings differ only by the parentheses `parenthesized` records.
8097                if *parenthesized {
8098                    f.write_str(" (LIKE ")?;
8099                    source.render(ctx, f)?;
8100                    f.write_str(")")
8101                } else {
8102                    f.write_str(" LIKE ")?;
8103                    source.render(ctx, f)
8104                }
8105            }
8106        }
8107    }
8108}
8109
8110impl<X: Extension + Render> Render for PartitionSpec<X> {
8111    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8112        self.strategy.render(ctx, f)?;
8113        f.write_str(" (")?;
8114        render_comma_separated(&self.columns, ctx, f)?;
8115        f.write_str(")")
8116    }
8117}
8118
8119impl Render for PartitionStrategy {
8120    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8121        f.write_str(match self {
8122            Self::List => "LIST",
8123            Self::Range => "RANGE",
8124            Self::Hash => "HASH",
8125        })
8126    }
8127}
8128
8129impl<X: Extension + Render> Render for PartitionElem<X> {
8130    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8131        // A parenthesized key (`(a + b)`) re-adds the grammar-mandated wrapping parentheses; a
8132        // bare column / function-call key renders unwrapped (its own binding power never needs
8133        // them here). The flag, not the expression kind, drives this so `(a)` and `a` round-trip
8134        // distinctly.
8135        if self.parenthesized {
8136            f.write_str("(")?;
8137            self.expr.render(ctx, f)?;
8138            f.write_str(")")?;
8139        } else {
8140            self.expr.render(ctx, f)?;
8141        }
8142        if let Some(collation) = &self.collation {
8143            f.write_str(" COLLATE ")?;
8144            collation.render(ctx, f)?;
8145        }
8146        if let Some(opclass) = &self.opclass {
8147            f.write_str(" ")?;
8148            opclass.render(ctx, f)?;
8149        }
8150        Ok(())
8151    }
8152}
8153
8154impl<X: Extension + Render> Render for PartitionBound<X> {
8155    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8156        match self {
8157            Self::List { values, .. } => {
8158                f.write_str("FOR VALUES IN (")?;
8159                render_comma_separated(values, ctx, f)?;
8160                f.write_str(")")
8161            }
8162            Self::Range { from, to, .. } => {
8163                f.write_str("FOR VALUES FROM (")?;
8164                render_comma_separated(from, ctx, f)?;
8165                f.write_str(") TO (")?;
8166                render_comma_separated(to, ctx, f)?;
8167                f.write_str(")")
8168            }
8169            Self::Hash {
8170                modulus, remainder, ..
8171            } => {
8172                f.write_str("FOR VALUES WITH (MODULUS ")?;
8173                modulus.render(ctx, f)?;
8174                f.write_str(", REMAINDER ")?;
8175                remainder.render(ctx, f)?;
8176                f.write_str(")")
8177            }
8178            Self::Default { .. } => f.write_str("DEFAULT"),
8179        }
8180    }
8181}
8182
8183fn render_ctas_columns(
8184    columns: &[Ident],
8185    ctx: &RenderCtx<'_>,
8186    f: &mut fmt::Formatter<'_>,
8187) -> fmt::Result {
8188    if !columns.is_empty() {
8189        f.write_str(" (")?;
8190        render_ident_list(columns, ctx, f)?;
8191        f.write_str(")")?;
8192    }
8193    Ok(())
8194}
8195
8196fn render_ctas_query_tail<X: Extension + Render>(
8197    query: &Query<X>,
8198    with_data: Option<bool>,
8199    ctx: &RenderCtx<'_>,
8200    f: &mut fmt::Formatter<'_>,
8201) -> fmt::Result {
8202    f.write_str(" AS ")?;
8203    query.render(ctx, f)?;
8204    render_with_data(with_data, f)
8205}
8206
8207/// Render the trailing ` WITH DATA` / ` WITH NO DATA` populate-option of a CTAS or
8208/// materialized-view body, or nothing when unspecified. The leading space is part
8209/// of the clause separator.
8210fn render_with_data(with_data: Option<bool>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8211    match with_data {
8212        Some(true) => f.write_str(" WITH DATA"),
8213        Some(false) => f.write_str(" WITH NO DATA"),
8214        None => Ok(()),
8215    }
8216}
8217
8218impl<X: Extension + Render> Render for TableElement<X> {
8219    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8220        match self {
8221            Self::Column { column, .. } => column.render(ctx, f),
8222            Self::Constraint { constraint, .. } => constraint.render(ctx, f),
8223            Self::Like {
8224                source, options, ..
8225            } => {
8226                f.write_str("LIKE ")?;
8227                source.render(ctx, f)?;
8228                for option in options {
8229                    f.write_str(" ")?;
8230                    option.render(ctx, f)?;
8231                }
8232                Ok(())
8233            }
8234        }
8235    }
8236}
8237
8238impl Render for TableLikeOption {
8239    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8240        self.action.render(ctx, f)?;
8241        f.write_str(" ")?;
8242        self.feature.render(ctx, f)
8243    }
8244}
8245
8246impl Render for TableLikeAction {
8247    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8248        f.write_str(match self {
8249            Self::Including => "INCLUDING",
8250            Self::Excluding => "EXCLUDING",
8251        })
8252    }
8253}
8254
8255impl Render for TableLikeFeature {
8256    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8257        f.write_str(match self {
8258            Self::Comments => "COMMENTS",
8259            Self::Compression => "COMPRESSION",
8260            Self::Constraints => "CONSTRAINTS",
8261            Self::Defaults => "DEFAULTS",
8262            Self::Generated => "GENERATED",
8263            Self::Identity => "IDENTITY",
8264            Self::Indexes => "INDEXES",
8265            Self::Statistics => "STATISTICS",
8266            Self::Storage => "STORAGE",
8267            Self::All => "ALL",
8268        })
8269    }
8270}
8271
8272impl<X: Extension + Render> Render for ColumnDef<X> {
8273    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8274        self.name.render(ctx, f)?;
8275        // A SQLite typeless column omits the type entirely (`a`, then its constraints),
8276        // so the name-to-type separator is written only when a type is present.
8277        // PostgreSQL grammar order keeps STORAGE/COMPRESSION before constraints.
8278        render_column_def_tail(self, ctx, f)
8279    }
8280}
8281
8282impl<X: Extension + Render> Render for ColumnConstraint<X> {
8283    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8284        if let Some(name) = &self.name {
8285            f.write_str("CONSTRAINT ")?;
8286            name.render(ctx, f)?;
8287            // A `Bare` constraint has no element after the name, so no separating space —
8288            // the `Bare` render arm below writes nothing.
8289            if !matches!(self.option, ColumnOption::Bare { .. }) {
8290                f.write_str(" ")?;
8291            }
8292        }
8293        self.option.render(ctx, f)?;
8294        if let Some(conflict) = self.conflict {
8295            f.write_str(" ON CONFLICT ")?;
8296            conflict.render(ctx, f)?;
8297        }
8298        render_constraint_characteristics(&self.characteristics, ctx, f)
8299    }
8300}
8301
8302/// Render a trailing ` DEFERRABLE`/` INITIALLY …` characteristics clause when
8303/// present; the leading space is part of the clause separator.
8304fn render_constraint_characteristics(
8305    characteristics: &Option<Box<ConstraintCharacteristics>>,
8306    ctx: &RenderCtx<'_>,
8307    f: &mut fmt::Formatter<'_>,
8308) -> fmt::Result {
8309    if let Some(characteristics) = characteristics {
8310        f.write_str(" ")?;
8311        characteristics.render(ctx, f)?;
8312    }
8313    Ok(())
8314}
8315
8316impl Render for ConstraintCharacteristics {
8317    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8318        // A separator is needed only between two written clauses, so the second
8319        // clause carries its own leading space when the first was also present.
8320        let mut written = false;
8321        if let Some(deferrable) = self.deferrable {
8322            f.write_str(if deferrable {
8323                "DEFERRABLE"
8324            } else {
8325                "NOT DEFERRABLE"
8326            })?;
8327            written = true;
8328        }
8329        if let Some(initially_deferred) = self.initially_deferred {
8330            if written {
8331                f.write_str(" ")?;
8332            }
8333            f.write_str(if initially_deferred {
8334                "INITIALLY DEFERRED"
8335            } else {
8336                "INITIALLY IMMEDIATE"
8337            })?;
8338        }
8339        Ok(())
8340    }
8341}
8342
8343impl<X: Extension + Render> Render for ColumnOption<X> {
8344    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8345        match self {
8346            Self::Null { .. } => f.write_str("NULL"),
8347            Self::NotNull { .. } => f.write_str("NOT NULL"),
8348            Self::Default { expr, .. } => {
8349                f.write_str("DEFAULT ")?;
8350                expr.render(ctx, f)
8351            }
8352            Self::Generated { generated, .. } => generated.render(ctx, f),
8353            Self::Identity { identity, .. } => identity.render(ctx, f),
8354            Self::PrimaryKey {
8355                ascending,
8356                index_tablespace,
8357                ..
8358            } => {
8359                f.write_str("PRIMARY KEY")?;
8360                match ascending {
8361                    Some(true) => f.write_str(" ASC")?,
8362                    Some(false) => f.write_str(" DESC")?,
8363                    None => {}
8364                }
8365                render_using_index_tablespace(index_tablespace.as_deref(), ctx, f)
8366            }
8367            Self::Unique {
8368                nulls_not_distinct,
8369                index_tablespace,
8370                ..
8371            } => {
8372                f.write_str("UNIQUE")?;
8373                render_nulls_not_distinct(*nulls_not_distinct, f)?;
8374                render_using_index_tablespace(index_tablespace.as_deref(), ctx, f)
8375            }
8376            Self::AutoIncrement { spelling, .. } => spelling.render(ctx, f),
8377            Self::Collate { collation, .. } => {
8378                f.write_str("COLLATE ")?;
8379                collation.render(ctx, f)
8380            }
8381            Self::Check {
8382                expr, no_inherit, ..
8383            } => {
8384                f.write_str("CHECK (")?;
8385                expr.render(ctx, f)?;
8386                f.write_str(")")?;
8387                if *no_inherit {
8388                    f.write_str(" NO INHERIT")?;
8389                }
8390                Ok(())
8391            }
8392            Self::References { reference, .. } => reference.render(ctx, f),
8393            // No element text — the enclosing `ColumnConstraint` render skips the separating
8394            // space it would otherwise write between the name and this element.
8395            Self::Bare { .. } => Ok(()),
8396            Self::Other { ext, .. } => ext.render(ctx, f),
8397        }
8398    }
8399}
8400
8401impl Render for AutoIncrementSpelling {
8402    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8403        f.write_str(match self {
8404            Self::Underscored => "AUTO_INCREMENT",
8405            Self::Joined => "AUTOINCREMENT",
8406        })
8407    }
8408}
8409
8410impl Render for ForeignKeyRef {
8411    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8412        f.write_str("REFERENCES ")?;
8413        self.table.render(ctx, f)?;
8414        if !self.columns.is_empty() {
8415            f.write_str(" (")?;
8416            render_ident_list(&self.columns, ctx, f)?;
8417            f.write_str(")")?;
8418        }
8419        // Canonical order: MATCH, then ON DELETE, then ON UPDATE. The clauses parse
8420        // order-independently, so the delete/update order is a normalization — replayed
8421        // only by a source-fidelity render honouring the `update_before_delete` tag.
8422        if let Some(match_type) = self.match_type {
8423            f.write_str(" MATCH ")?;
8424            match_type.render(ctx, f)?;
8425        }
8426        let render_on_delete = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
8427            if let Some(on_delete) = &self.on_delete {
8428                f.write_str(" ON DELETE ")?;
8429                on_delete.render(ctx, f)?;
8430            }
8431            Ok(())
8432        };
8433        let render_on_update = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
8434            if let Some(on_update) = &self.on_update {
8435                f.write_str(" ON UPDATE ")?;
8436                on_update.render(ctx, f)?;
8437            }
8438            Ok(())
8439        };
8440        if self.update_before_delete && honours_source_spelling(ctx) {
8441            render_on_update(f)?;
8442            render_on_delete(f)?;
8443        } else {
8444            render_on_delete(f)?;
8445            render_on_update(f)?;
8446        }
8447        Ok(())
8448    }
8449}
8450
8451impl Render for ForeignKeyMatch {
8452    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8453        f.write_str(match self {
8454            Self::Full => "FULL",
8455            Self::Partial => "PARTIAL",
8456            Self::Simple => "SIMPLE",
8457        })
8458    }
8459}
8460
8461impl Render for ReferentialAction {
8462    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8463        match self {
8464            Self::NoAction { .. } => f.write_str("NO ACTION"),
8465            Self::Restrict { .. } => f.write_str("RESTRICT"),
8466            Self::Cascade { .. } => f.write_str("CASCADE"),
8467            Self::SetNull { columns, .. } => render_set_action(ctx, f, "SET NULL", columns),
8468            Self::SetDefault { columns, .. } => render_set_action(ctx, f, "SET DEFAULT", columns),
8469        }
8470    }
8471}
8472
8473/// Render `SET NULL` / `SET DEFAULT` with its optional `(col, ...)` column list.
8474fn render_set_action(
8475    ctx: &RenderCtx<'_>,
8476    f: &mut fmt::Formatter<'_>,
8477    keyword: &str,
8478    columns: &[Ident],
8479) -> fmt::Result {
8480    f.write_str(keyword)?;
8481    if !columns.is_empty() {
8482        f.write_str(" (")?;
8483        render_ident_list(columns, ctx, f)?;
8484        f.write_str(")")?;
8485    }
8486    Ok(())
8487}
8488
8489impl<X: Extension + Render> Render for GeneratedColumn<X> {
8490    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8491        // The surface tag restores the source spelling: the standard keyworded form or
8492        // the MySQL/SQLite keywordless `AS (…)` shorthand (ADR-0011).
8493        f.write_str(match self.spelling {
8494            GeneratedColumnSpelling::GeneratedAlways => "GENERATED ALWAYS AS (",
8495            GeneratedColumnSpelling::Shorthand => "AS (",
8496        })?;
8497        self.expr.render(ctx, f)?;
8498        f.write_str(")")?;
8499        if let Some(storage) = self.storage {
8500            f.write_str(" ")?;
8501            storage.render(ctx, f)?;
8502        }
8503        Ok(())
8504    }
8505}
8506
8507impl Render for GeneratedColumnStorage {
8508    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8509        f.write_str(match self {
8510            Self::Stored => "STORED",
8511            Self::Virtual => "VIRTUAL",
8512        })
8513    }
8514}
8515
8516impl<X: Extension + Render> Render for IdentityColumn<X> {
8517    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8518        f.write_str("GENERATED ")?;
8519        self.generation.render(ctx, f)?;
8520        f.write_str(" AS IDENTITY")?;
8521        if !self.options.is_empty() {
8522            f.write_str(" (")?;
8523            for (index, option) in self.options.iter().enumerate() {
8524                if index > 0 {
8525                    f.write_str(" ")?;
8526                }
8527                option.render(ctx, f)?;
8528            }
8529            f.write_str(")")?;
8530        }
8531        Ok(())
8532    }
8533}
8534
8535impl Render for IdentityGeneration {
8536    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8537        f.write_str(match self {
8538            Self::Always => "ALWAYS",
8539            Self::ByDefault => "BY DEFAULT",
8540        })
8541    }
8542}
8543
8544impl<X: Extension + Render> Render for IdentityOption<X> {
8545    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8546        match self {
8547            Self::StartWith { expr, .. } => {
8548                f.write_str("START WITH ")?;
8549                expr.render(ctx, f)
8550            }
8551            Self::IncrementBy { expr, .. } => {
8552                f.write_str("INCREMENT BY ")?;
8553                expr.render(ctx, f)
8554            }
8555            Self::MinValue {
8556                value: Some(expr), ..
8557            } => {
8558                f.write_str("MINVALUE ")?;
8559                expr.render(ctx, f)
8560            }
8561            Self::MinValue { value: None, .. } => f.write_str("NO MINVALUE"),
8562            Self::MaxValue {
8563                value: Some(expr), ..
8564            } => {
8565                f.write_str("MAXVALUE ")?;
8566                expr.render(ctx, f)
8567            }
8568            Self::MaxValue { value: None, .. } => f.write_str("NO MAXVALUE"),
8569            Self::Cache { expr, .. } => {
8570                f.write_str("CACHE ")?;
8571                expr.render(ctx, f)
8572            }
8573            Self::Cycle { cycle: true, .. } => f.write_str("CYCLE"),
8574            Self::Cycle { cycle: false, .. } => f.write_str("NO CYCLE"),
8575        }
8576    }
8577}
8578
8579impl<X: Extension + Render> Render for TableConstraintDef<X> {
8580    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8581        if let Some(name) = &self.name {
8582            f.write_str("CONSTRAINT ")?;
8583            name.render(ctx, f)?;
8584            // A `Bare` constraint has no element after the name, so no separating space —
8585            // the `Bare` render arm below writes nothing.
8586            if !matches!(self.constraint, TableConstraint::Bare { .. }) {
8587                f.write_str(" ")?;
8588            }
8589        }
8590        self.constraint.render(ctx, f)?;
8591        // The `NO INHERIT` / `NOT VALID` markers share PostgreSQL's constraint-attribute slot
8592        // with the deferral characteristics; a fixed canonical order round-trips (PostgreSQL
8593        // accepts the markers in any order).
8594        if self.no_inherit {
8595            f.write_str(" NO INHERIT")?;
8596        }
8597        if self.not_valid {
8598            f.write_str(" NOT VALID")?;
8599        }
8600        render_constraint_characteristics(&self.characteristics, ctx, f)
8601    }
8602}
8603
8604impl<X: Extension + Render> Render for TableConstraint<X> {
8605    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8606        match self {
8607            Self::PrimaryKey {
8608                columns, include, ..
8609            } => {
8610                f.write_str("PRIMARY KEY (")?;
8611                render_comma_separated(columns, ctx, f)?;
8612                f.write_str(")")?;
8613                render_include_columns(include, ctx, f)
8614            }
8615            Self::Unique {
8616                columns,
8617                nulls_not_distinct,
8618                include,
8619                ..
8620            } => {
8621                f.write_str("UNIQUE")?;
8622                render_nulls_not_distinct(*nulls_not_distinct, f)?;
8623                f.write_str(" (")?;
8624                render_comma_separated(columns, ctx, f)?;
8625                f.write_str(")")?;
8626                render_include_columns(include, ctx, f)
8627            }
8628            Self::Check { expr, .. } => {
8629                f.write_str("CHECK (")?;
8630                expr.render(ctx, f)?;
8631                f.write_str(")")
8632            }
8633            Self::Exclude { exclude, .. } => exclude.render(ctx, f),
8634            Self::ForeignKey {
8635                columns,
8636                references,
8637                ..
8638            } => {
8639                f.write_str("FOREIGN KEY (")?;
8640                render_ident_list(columns, ctx, f)?;
8641                f.write_str(") ")?;
8642                references.render(ctx, f)
8643            }
8644            // No element text — the enclosing `TableConstraintDef` render skips the separating
8645            // space it would otherwise write between the name and this element.
8646            Self::Bare { .. } => Ok(()),
8647            Self::Other { ext, .. } => ext.render(ctx, f),
8648        }
8649    }
8650}
8651
8652/// Render an `INCLUDE (<col>, ...)` covering-column list; the leading space is part of the
8653/// clause separator, and an empty list (no clause) renders nothing.
8654fn render_include_columns(
8655    include: &[Ident],
8656    ctx: &RenderCtx<'_>,
8657    f: &mut fmt::Formatter<'_>,
8658) -> fmt::Result {
8659    if !include.is_empty() {
8660        f.write_str(" INCLUDE (")?;
8661        render_ident_list(include, ctx, f)?;
8662        f.write_str(")")?;
8663    }
8664    Ok(())
8665}
8666
8667/// Render the ` NULLS [NOT] DISTINCT` null-treatment; the leading space is part of the clause
8668/// separator, and an unwritten treatment (`None`) renders nothing.
8669fn render_nulls_not_distinct(
8670    nulls_not_distinct: Option<bool>,
8671    f: &mut fmt::Formatter<'_>,
8672) -> fmt::Result {
8673    match nulls_not_distinct {
8674        Some(false) => f.write_str(" NULLS NOT DISTINCT"),
8675        Some(true) => f.write_str(" NULLS DISTINCT"),
8676        None => Ok(()),
8677    }
8678}
8679
8680/// Render the ` USING INDEX TABLESPACE <name>` index-parameter clause when present; the leading
8681/// space is part of the clause separator.
8682fn render_using_index_tablespace(
8683    tablespace: Option<&Ident>,
8684    ctx: &RenderCtx<'_>,
8685    f: &mut fmt::Formatter<'_>,
8686) -> fmt::Result {
8687    if let Some(tablespace) = tablespace {
8688        f.write_str(" USING INDEX TABLESPACE ")?;
8689        tablespace.render(ctx, f)?;
8690    }
8691    Ok(())
8692}
8693
8694impl<X: Extension + Render> Render for ExcludeConstraint<X> {
8695    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8696        f.write_str("EXCLUDE")?;
8697        if let Some(method) = &self.method {
8698            f.write_str(" USING ")?;
8699            method.render(ctx, f)?;
8700        }
8701        f.write_str(" (")?;
8702        render_comma_separated(&self.elements, ctx, f)?;
8703        f.write_str(")")?;
8704        render_include_columns(&self.include, ctx, f)?;
8705        if !self.with_params.is_empty() {
8706            f.write_str(" WITH (")?;
8707            render_comma_separated(&self.with_params, ctx, f)?;
8708            f.write_str(")")?;
8709        }
8710        render_using_index_tablespace(self.index_tablespace.as_ref(), ctx, f)?;
8711        if let Some(predicate) = &self.predicate {
8712            f.write_str(" WHERE (")?;
8713            predicate.render(ctx, f)?;
8714            f.write_str(")")?;
8715        }
8716        Ok(())
8717    }
8718}
8719
8720impl<X: Extension + Render> Render for ExcludeElement<X> {
8721    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8722        // A parenthesized key re-adds the grammar-mandated wrapping parentheses; a bare column /
8723        // function-call key renders unwrapped, exactly as `PartitionElem`.
8724        if self.parenthesized {
8725            f.write_str("(")?;
8726            self.expr.render(ctx, f)?;
8727            f.write_str(")")?;
8728        } else {
8729            self.expr.render(ctx, f)?;
8730        }
8731        if let Some(collation) = &self.collation {
8732            f.write_str(" COLLATE ")?;
8733            collation.render(ctx, f)?;
8734        }
8735        if let Some(opclass) = &self.opclass {
8736            f.write_str(" ")?;
8737            opclass.render(ctx, f)?;
8738            if !self.opclass_params.is_empty() {
8739                f.write_str(" (")?;
8740                render_comma_separated(&self.opclass_params, ctx, f)?;
8741                f.write_str(")")?;
8742            }
8743        }
8744        match self.asc {
8745            Some(true) => f.write_str(" ASC")?,
8746            Some(false) => f.write_str(" DESC")?,
8747            None => {}
8748        }
8749        match self.nulls_first {
8750            Some(true) => f.write_str(" NULLS FIRST")?,
8751            Some(false) => f.write_str(" NULLS LAST")?,
8752            None => {}
8753        }
8754        f.write_str(" WITH ")?;
8755        self.operator.render(ctx, f)
8756    }
8757}
8758
8759impl Render for ExcludeOperator {
8760    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8761        match self.spelling {
8762            // The bare form is always unqualified: `a WITH &&`.
8763            NamedOperatorSpelling::Bare => f.write_str(ctx.resolve(self.op)),
8764            // The `OPERATOR(...)` keyword form carries the optional schema: `a WITH
8765            // OPERATOR(pg_catalog.=)`.
8766            NamedOperatorSpelling::OperatorKeyword => {
8767                f.write_str("OPERATOR(")?;
8768                for part in &self.schema.0 {
8769                    part.render(ctx, f)?;
8770                    f.write_str(".")?;
8771                }
8772                f.write_str(ctx.resolve(self.op))?;
8773                f.write_str(")")
8774            }
8775        }
8776    }
8777}
8778
8779impl<X: Extension + Render> Render for CreateTableOption<X> {
8780    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8781        self.kind.render(ctx, f)
8782    }
8783}
8784
8785impl<X: Extension + Render> Render for CreateTableOptionKind<X> {
8786    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8787        match self {
8788            Self::ColocateWith { table, columns, .. } => {
8789                f.write_str("COLOCATE WITH ")?;
8790                table.render(ctx, f)?;
8791                f.write_str(" ON (")?;
8792                render_ident_list(columns, ctx, f)?;
8793                f.write_str(")")
8794            }
8795            Self::InColocationGroup { group, columns, .. } => {
8796                f.write_str("IN COLOCATION GROUP ")?;
8797                group.render(ctx, f)?;
8798                if !columns.is_empty() {
8799                    f.write_str(" ON (")?;
8800                    render_ident_list(columns, ctx, f)?;
8801                    f.write_str(")")?;
8802                }
8803                Ok(())
8804            }
8805            Self::With { params, .. } => {
8806                f.write_str("WITH (")?;
8807                render_comma_separated(params, ctx, f)?;
8808                f.write_str(")")
8809            }
8810            Self::OnCommit { action, .. } => {
8811                f.write_str("ON COMMIT ")?;
8812                action.render(ctx, f)
8813            }
8814            Self::Tablespace { tablespace, .. } => {
8815                f.write_str("TABLESPACE ")?;
8816                tablespace.render(ctx, f)
8817            }
8818            Self::KeyValue { option, .. } => option.render(ctx, f),
8819            Self::WithoutRowid { .. } => f.write_str("WITHOUT ROWID"),
8820            Self::Strict { .. } => f.write_str("STRICT"),
8821            Self::WithoutOids { .. } => f.write_str("WITHOUT OIDS"),
8822        }
8823    }
8824}
8825
8826impl Render for TableOption {
8827    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8828        // Canonical MySQL spelling: `<name> = <value>`. The optional source `=` and
8829        // the `DEFAULT` noise prefix are normalized to this fixed form (ADR-0011); the
8830        // ` = ` matches the `WITH` storage-parameter rendering.
8831        self.name.render(ctx, f)?;
8832        f.write_str(" = ")?;
8833        self.value.render(ctx, f)
8834    }
8835}
8836
8837impl Render for TableOptionValue {
8838    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8839        match self {
8840            Self::Word { word, .. } => word.render(ctx, f),
8841            Self::String { value, .. } | Self::Number { value, .. } => value.render(ctx, f),
8842        }
8843    }
8844}
8845
8846impl Render for OnCommitAction {
8847    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8848        f.write_str(match self {
8849            Self::PreserveRows => "PRESERVE ROWS",
8850            Self::DeleteRows => "DELETE ROWS",
8851            Self::Drop => "DROP",
8852        })
8853    }
8854}
8855
8856impl<X: Extension + Render> Render for TableStorageParameter<X> {
8857    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8858        self.name.render(ctx, f)?;
8859        if let Some(value) = &self.value {
8860            f.write_str(" = ")?;
8861            value.render(ctx, f)?;
8862        }
8863        Ok(())
8864    }
8865}
8866
8867impl<X: Extension + Render> Render for AlterTable<X> {
8868    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8869        f.write_str("ALTER TABLE")?;
8870        if self.if_exists {
8871            f.write_str(" IF EXISTS")?;
8872        }
8873        f.write_str(" ")?;
8874        self.name.render(ctx, f)?;
8875        f.write_str(" ")?;
8876        render_comma_separated(&self.actions, ctx, f)
8877    }
8878}
8879
8880impl Render for AlterColumnTarget {
8881    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8882        for (i, part) in self.parts.iter().enumerate() {
8883            if i > 0 {
8884                f.write_str(".")?;
8885            }
8886            part.render(ctx, f)?;
8887        }
8888        Ok(())
8889    }
8890}
8891
8892/// Render the optional `COLUMN` noise word (with its leading space) after an
8893/// `ADD`/`DROP`/`ALTER`/`RENAME` column action. The canonical render always emits it;
8894/// a source-fidelity render drops it when the source did (exact-synonym fidelity).
8895fn render_optional_column_keyword(
8896    column_keyword: bool,
8897    ctx: &RenderCtx<'_>,
8898    f: &mut fmt::Formatter<'_>,
8899) -> fmt::Result {
8900    if column_keyword || !honours_source_spelling(ctx) {
8901        f.write_str(" COLUMN")?;
8902    }
8903    Ok(())
8904}
8905
8906fn render_column_def_tail<X: Extension + Render>(
8907    column: &ColumnDef<X>,
8908    ctx: &RenderCtx<'_>,
8909    f: &mut fmt::Formatter<'_>,
8910) -> fmt::Result {
8911    if let Some(data_type) = &column.data_type {
8912        f.write_str(" ")?;
8913        data_type.render(ctx, f)?;
8914    }
8915    if let Some(storage) = &column.storage {
8916        f.write_str(" STORAGE ")?;
8917        storage.render(ctx, f)?;
8918    }
8919    if let Some(compression) = &column.compression {
8920        f.write_str(" COMPRESSION ")?;
8921        compression.render(ctx, f)?;
8922    }
8923    for constraint in &column.constraints {
8924        f.write_str(" ")?;
8925        constraint.render(ctx, f)?;
8926    }
8927    Ok(())
8928}
8929
8930impl<X: Extension + Render> Render for AlterTableAction<X> {
8931    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8932        match self {
8933            Self::SetColocationGroup { group, .. } => {
8934                f.write_str("SET COLOCATION GROUP ")?;
8935                group.render(ctx, f)
8936            }
8937            Self::DropColocationGroup { .. } => f.write_str("DROP COLOCATION GROUP"),
8938            // The optional `COLUMN` noise word is exact-synonym fidelity: the canonical
8939            // render emits it, a source-fidelity render drops it when the source did.
8940            Self::AddColumn {
8941                if_not_exists,
8942                column_keyword,
8943                target,
8944                column,
8945                ..
8946            } => {
8947                f.write_str("ADD")?;
8948                render_optional_column_keyword(*column_keyword, ctx, f)?;
8949                if *if_not_exists {
8950                    f.write_str(" IF NOT EXISTS")?;
8951                }
8952                f.write_str(" ")?;
8953                if let Some(target) = target {
8954                    target.render(ctx, f)?;
8955                    render_column_def_tail(column, ctx, f)
8956                } else {
8957                    column.render(ctx, f)
8958                }
8959            }
8960            Self::DropColumn {
8961                if_exists,
8962                column_keyword,
8963                name,
8964                behavior,
8965                ..
8966            } => {
8967                f.write_str("DROP")?;
8968                render_optional_column_keyword(*column_keyword, ctx, f)?;
8969                if *if_exists {
8970                    f.write_str(" IF EXISTS")?;
8971                }
8972                f.write_str(" ")?;
8973                name.render(ctx, f)?;
8974                render_drop_behavior(*behavior, ctx, f)
8975            }
8976            Self::AlterColumn {
8977                column_keyword,
8978                name,
8979                change,
8980                ..
8981            } => {
8982                f.write_str("ALTER")?;
8983                render_optional_column_keyword(*column_keyword, ctx, f)?;
8984                f.write_str(" ")?;
8985                name.render(ctx, f)?;
8986                f.write_str(" ")?;
8987                change.render(ctx, f)
8988            }
8989            Self::AddConstraint { constraint, .. } => {
8990                f.write_str("ADD ")?;
8991                constraint.render(ctx, f)
8992            }
8993            Self::DropConstraint {
8994                if_exists,
8995                name,
8996                behavior,
8997                ..
8998            } => {
8999                f.write_str("DROP CONSTRAINT")?;
9000                if *if_exists {
9001                    f.write_str(" IF EXISTS")?;
9002                }
9003                f.write_str(" ")?;
9004                name.render(ctx, f)?;
9005                render_drop_behavior(*behavior, ctx, f)
9006            }
9007            Self::DropPrimaryKey { behavior, .. } => {
9008                f.write_str("DROP PRIMARY KEY")?;
9009                render_drop_behavior(*behavior, ctx, f)
9010            }
9011            Self::SetOptions { params, .. } => {
9012                f.write_str("SET (")?;
9013                render_comma_separated(params, ctx, f)?;
9014                f.write_str(")")
9015            }
9016            Self::RenameColumn {
9017                column_keyword,
9018                name,
9019                new_name,
9020                ..
9021            } => {
9022                f.write_str("RENAME")?;
9023                render_optional_column_keyword(*column_keyword, ctx, f)?;
9024                f.write_str(" ")?;
9025                name.render(ctx, f)?;
9026                f.write_str(" TO ")?;
9027                new_name.render(ctx, f)
9028            }
9029            Self::RenameConstraint { name, new_name, .. } => {
9030                f.write_str("RENAME CONSTRAINT ")?;
9031                name.render(ctx, f)?;
9032                f.write_str(" TO ")?;
9033                new_name.render(ctx, f)
9034            }
9035            Self::RenameTable { new_name, .. } => {
9036                f.write_str("RENAME TO ")?;
9037                new_name.render(ctx, f)
9038            }
9039            Self::AttachPartition {
9040                partition, bound, ..
9041            } => {
9042                f.write_str("ATTACH PARTITION ")?;
9043                partition.render(ctx, f)?;
9044                f.write_str(" ")?;
9045                bound.render(ctx, f)
9046            }
9047            Self::DetachPartition {
9048                partition, mode, ..
9049            } => {
9050                f.write_str("DETACH PARTITION ")?;
9051                partition.render(ctx, f)?;
9052                match mode {
9053                    Some(DetachPartitionMode::Concurrently) => f.write_str(" CONCURRENTLY"),
9054                    Some(DetachPartitionMode::Finalize) => f.write_str(" FINALIZE"),
9055                    None => Ok(()),
9056                }
9057            }
9058        }
9059    }
9060}
9061
9062impl<X: Extension + Render> Render for AlterColumnAction<X> {
9063    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9064        match self {
9065            Self::SetDefault { expr, .. } => {
9066                f.write_str("SET DEFAULT ")?;
9067                expr.render(ctx, f)
9068            }
9069            Self::DropDefault { .. } => f.write_str("DROP DEFAULT"),
9070            Self::SetNotNull { .. } => f.write_str("SET NOT NULL"),
9071            Self::DropNotNull { .. } => f.write_str("DROP NOT NULL"),
9072            Self::AddIdentity { identity, .. } => {
9073                f.write_str("ADD ")?;
9074                identity.render(ctx, f)
9075            }
9076            // The ANSI `SET DATA TYPE` spelling is canonical; a source-fidelity render
9077            // replays the bare PostgreSQL `TYPE` when the source wrote it.
9078            Self::SetDataType {
9079                set_data,
9080                data_type,
9081                using,
9082                ..
9083            } => {
9084                if *set_data || !honours_source_spelling(ctx) {
9085                    f.write_str("SET DATA TYPE ")?;
9086                } else {
9087                    f.write_str("TYPE ")?;
9088                }
9089                data_type.render(ctx, f)?;
9090                if let Some(using) = using {
9091                    f.write_str(" USING ")?;
9092                    using.render(ctx, f)?;
9093                }
9094                Ok(())
9095            }
9096        }
9097    }
9098}
9099
9100impl Render for DropStatement {
9101    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9102        f.write_str("DROP ")?;
9103        self.object_kind.render(ctx, f)?;
9104        if self.if_exists {
9105            f.write_str(" IF EXISTS")?;
9106        }
9107        f.write_str(" ")?;
9108        render_comma_separated(&self.names, ctx, f)?;
9109        render_drop_behavior(self.behavior, ctx, f)
9110    }
9111}
9112
9113impl Render for DropObjectKind {
9114    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9115        f.write_str(match self {
9116            Self::Table => "TABLE",
9117            Self::View => "VIEW",
9118            Self::MaterializedView => "MATERIALIZED VIEW",
9119            Self::Index => "INDEX",
9120            Self::Schema => "SCHEMA",
9121            Self::Type => "TYPE",
9122            Self::Sequence => "SEQUENCE",
9123            Self::Macro => "MACRO",
9124            Self::MacroTable => "MACRO TABLE",
9125            Self::Trigger => "TRIGGER",
9126        })
9127    }
9128}
9129
9130impl Render for DropBehavior {
9131    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9132        f.write_str(match self {
9133            Self::Cascade => "CASCADE",
9134            Self::Restrict => "RESTRICT",
9135        })
9136    }
9137}
9138
9139/// Render a trailing ` CASCADE` / ` RESTRICT` drop behaviour when present; the
9140/// leading space is part of the clause separator.
9141fn render_drop_behavior(
9142    behavior: Option<DropBehavior>,
9143    ctx: &RenderCtx<'_>,
9144    f: &mut fmt::Formatter<'_>,
9145) -> fmt::Result {
9146    if let Some(behavior) = behavior {
9147        f.write_str(" ")?;
9148        behavior.render(ctx, f)?;
9149    }
9150    Ok(())
9151}
9152
9153impl<X: Extension + Render> Render for CreateSchema<X> {
9154    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9155        f.write_str("CREATE SCHEMA")?;
9156        if self.if_not_exists {
9157            f.write_str(" IF NOT EXISTS")?;
9158        }
9159        if let Some(name) = &self.name {
9160            f.write_str(" ")?;
9161            name.render(ctx, f)?;
9162        }
9163        if let Some(authorization) = &self.authorization {
9164            f.write_str(" AUTHORIZATION ")?;
9165            authorization.render(ctx, f)?;
9166        }
9167        // Embedded schema elements render space-separated after the head (no `;`), so
9168        // the whole `CREATE SCHEMA s CREATE TABLE t ...` stays one rendered statement —
9169        // preserving the statement count and round-tripping through the parser's
9170        // schema-element loop.
9171        for element in &self.elements {
9172            f.write_str(" ")?;
9173            element.render(ctx, f)?;
9174        }
9175        Ok(())
9176    }
9177}
9178
9179impl<X: Extension + Render> Render for CreateView<X> {
9180    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9181        f.write_str("CREATE")?;
9182        if self.or_replace {
9183            f.write_str(" OR REPLACE")?;
9184        }
9185        if let Some(temporary) = self.temporary {
9186            f.write_str(" ")?;
9187            temporary.render(ctx, f)?;
9188        }
9189        if self.materialized {
9190            f.write_str(" MATERIALIZED")?;
9191        }
9192        // `RECURSIVE` sits between the `TEMP`/`TEMPORARY` prefix and `VIEW`; it never
9193        // co-occurs with `MATERIALIZED` (parser-enforced), so the order is unambiguous.
9194        if self.recursive {
9195            f.write_str(" RECURSIVE")?;
9196        }
9197        // The MySQL `[ALGORITHM = …] [DEFINER = …] [SQL SECURITY …]` prefix, between `OR
9198        // REPLACE` and `VIEW`. All-`None` (every non-MySQL view) emits nothing.
9199        self.options.render(ctx, f)?;
9200        f.write_str(" VIEW")?;
9201        if self.if_not_exists {
9202            f.write_str(" IF NOT EXISTS")?;
9203        }
9204        f.write_str(" ")?;
9205        self.name.render(ctx, f)?;
9206        if !self.columns.is_empty() {
9207            f.write_str(" (")?;
9208            render_ident_list(&self.columns, ctx, f)?;
9209            f.write_str(")")?;
9210        }
9211        if let Some(to) = &self.to {
9212            f.write_str(" TO ")?;
9213            to.render(ctx, f)?;
9214        }
9215        f.write_str(" AS ")?;
9216        self.query.render(ctx, f)?;
9217        if let Some(check_option) = self.check_option {
9218            f.write_str(" ")?;
9219            check_option.render(ctx, f)?;
9220        }
9221        render_with_data(self.with_data, f)
9222    }
9223}
9224
9225impl<X: Extension + Render> Render for AlterView<X> {
9226    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9227        f.write_str("ALTER")?;
9228        // The `[ALGORITHM = …] [DEFINER = …] [SQL SECURITY …]` prefix, between `ALTER` and
9229        // `VIEW`. All-`None` (a bare `ALTER VIEW`) emits nothing.
9230        self.options.render(ctx, f)?;
9231        f.write_str(" VIEW ")?;
9232        self.name.render(ctx, f)?;
9233        if !self.columns.is_empty() {
9234            f.write_str(" (")?;
9235            render_ident_list(&self.columns, ctx, f)?;
9236            f.write_str(")")?;
9237        }
9238        f.write_str(" AS ")?;
9239        self.query.render(ctx, f)?;
9240        if let Some(check_option) = self.check_option {
9241            f.write_str(" ")?;
9242            check_option.render(ctx, f)?;
9243        }
9244        Ok(())
9245    }
9246}
9247
9248impl Render for ViewOptions {
9249    /// Emit each present option with a *leading* space, so the prefix slots between the
9250    /// `CREATE`/`ALTER` keyword and `VIEW`. All-`None` emits nothing (every non-MySQL view).
9251    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9252        if let Some(algorithm) = self.algorithm {
9253            f.write_str(" ALGORITHM = ")?;
9254            algorithm.render(ctx, f)?;
9255        }
9256        if let Some(definer) = &self.definer {
9257            f.write_str(" ")?;
9258            definer.render(ctx, f)?;
9259        }
9260        if let Some(sql_security) = self.sql_security {
9261            f.write_str(" SQL SECURITY ")?;
9262            sql_security.render(ctx, f)?;
9263        }
9264        Ok(())
9265    }
9266}
9267
9268impl Render for ViewAlgorithm {
9269    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9270        f.write_str(match self {
9271            Self::Undefined => "UNDEFINED",
9272            Self::Merge => "MERGE",
9273            Self::TempTable => "TEMPTABLE",
9274        })
9275    }
9276}
9277
9278impl<X: Extension + Render> Render for CreateSecret<X> {
9279    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9280        f.write_str("CREATE ")?;
9281        if self.persistent {
9282            f.write_str("PERSISTENT ")?;
9283        }
9284        f.write_str("SECRET ")?;
9285        self.name.render(ctx, f)?;
9286        f.write_str(" (")?;
9287        render_comma_separated(&self.options, ctx, f)?;
9288        f.write_str(")")
9289    }
9290}
9291
9292impl<X: Extension + Render> Render for SecretOption<X> {
9293    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9294        self.name.render(ctx, f)?;
9295        f.write_str(" ")?;
9296        self.value.render(ctx, f)
9297    }
9298}
9299
9300impl Render for DropSecretStmt {
9301    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9302        f.write_str("DROP ")?;
9303        match self.persistence {
9304            SecretPersistence::Default => {}
9305            SecretPersistence::Temporary => f.write_str("TEMPORARY ")?,
9306            SecretPersistence::Persistent => f.write_str("PERSISTENT ")?,
9307        }
9308        f.write_str("SECRET ")?;
9309        if self.if_exists {
9310            f.write_str("IF EXISTS ")?;
9311        }
9312        self.name.render(ctx, f)?;
9313        if let Some(storage) = &self.storage {
9314            f.write_str(" FROM ")?;
9315            storage.render(ctx, f)?;
9316        }
9317        Ok(())
9318    }
9319}
9320
9321impl<X: Extension + Render> Render for CreateType<X> {
9322    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9323        f.write_str("CREATE")?;
9324        if self.or_replace {
9325            f.write_str(" OR REPLACE")?;
9326        }
9327        if let Some(temporary) = self.temporary {
9328            f.write_str(" ")?;
9329            temporary.render(ctx, f)?;
9330        }
9331        f.write_str(" TYPE")?;
9332        if self.if_not_exists {
9333            f.write_str(" IF NOT EXISTS")?;
9334        }
9335        f.write_str(" ")?;
9336        self.name.render(ctx, f)?;
9337        f.write_str(" AS ")?;
9338        self.definition.render(ctx, f)
9339    }
9340}
9341
9342impl Render for CreateVirtualTable {
9343    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9344        f.write_str("CREATE VIRTUAL TABLE ")?;
9345        if self.if_not_exists {
9346            f.write_str("IF NOT EXISTS ")?;
9347        }
9348        self.name.render(ctx, f)?;
9349        f.write_str(" USING ")?;
9350        self.module.render(ctx, f)?;
9351        if let Some(args) = &self.args {
9352            f.write_str("(")?;
9353            render_comma_separated(args, ctx, f)?;
9354            f.write_str(")")?;
9355        }
9356        Ok(())
9357    }
9358}
9359
9360impl Render for ModuleArg {
9361    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9362        // The verbatim slice is opaque module-owned content (column names, quoted
9363        // option strings) that can carry PII, so Redacted rendering masks each argument
9364        // to one placeholder — arg arity (query shape) survives, the text does not
9365        // (ADR-0010), mirroring how `Literal` masks values to `?`.
9366        if ctx.mode() == RenderMode::Redacted {
9367            return f.write_str("?");
9368        }
9369        // Otherwise emit the interned source slice as-is: the module (not this parser)
9370        // owns the argument grammar, so the text round-trips verbatim.
9371        f.write_str(ctx.resolve(self.text))
9372    }
9373}
9374
9375impl<X: Extension + Render> Render for CreateSequence<X> {
9376    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9377        f.write_str("CREATE")?;
9378        if let Some(temporary) = self.temporary {
9379            f.write_str(" ")?;
9380            temporary.render(ctx, f)?;
9381        }
9382        f.write_str(" SEQUENCE")?;
9383        if self.if_not_exists {
9384            f.write_str(" IF NOT EXISTS")?;
9385        }
9386        f.write_str(" ")?;
9387        self.name.render(ctx, f)?;
9388        // The options are space-separated in the canonical `START WITH`/`INCREMENT BY`
9389        // spelling (which both engines accept); reuse the shared `IdentityOption` render.
9390        for option in &self.options {
9391            f.write_str(" ")?;
9392            option.render(ctx, f)?;
9393        }
9394        Ok(())
9395    }
9396}
9397
9398impl Render for CreateExtension {
9399    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9400        f.write_str("CREATE EXTENSION")?;
9401        if self.if_not_exists {
9402            f.write_str(" IF NOT EXISTS")?;
9403        }
9404        f.write_str(" ")?;
9405        self.name.render(ctx, f)?;
9406        // `WITH` is optional sugar; replay it only when the source wrote it.
9407        if self.with {
9408            f.write_str(" WITH")?;
9409        }
9410        for option in &self.options {
9411            f.write_str(" ")?;
9412            option.render(ctx, f)?;
9413        }
9414        Ok(())
9415    }
9416}
9417
9418impl Render for CreateExtensionOption {
9419    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9420        match self {
9421            Self::Schema { name, .. } => {
9422                f.write_str("SCHEMA ")?;
9423                name.render(ctx, f)
9424            }
9425            Self::Version { version, .. } => {
9426                f.write_str("VERSION ")?;
9427                version.render(ctx, f)
9428            }
9429            Self::Cascade { .. } => f.write_str("CASCADE"),
9430        }
9431    }
9432}
9433
9434impl Render for ExtensionVersion {
9435    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9436        match self {
9437            Self::Word { word, .. } => word.render(ctx, f),
9438            Self::String { value, .. } => value.render(ctx, f),
9439        }
9440    }
9441}
9442
9443impl<X: Extension + Render> Render for AlterExtension<X> {
9444    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9445        f.write_str("ALTER EXTENSION ")?;
9446        self.name.render(ctx, f)?;
9447        f.write_str(" ")?;
9448        self.action.render(ctx, f)
9449    }
9450}
9451
9452impl<X: Extension + Render> Render for AlterExtensionAction<X> {
9453    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9454        match self {
9455            Self::Update { version, .. } => {
9456                f.write_str("UPDATE")?;
9457                if let Some(version) = version {
9458                    f.write_str(" TO ")?;
9459                    version.render(ctx, f)?;
9460                }
9461                Ok(())
9462            }
9463            Self::Change { add, member, .. } => {
9464                f.write_str(if *add { "ADD " } else { "DROP " })?;
9465                member.render(ctx, f)
9466            }
9467        }
9468    }
9469}
9470
9471impl Render for SizeLiteral {
9472    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9473        // The exact source spelling (digits + any suffix, and its case) round-trips from the
9474        // whole-literal span, exactly as a `Literal` does.
9475        if let Some(text) = ctx.slice(self.meta.span) {
9476            return f.write_str(text);
9477        }
9478        // A synthesized or detached literal has no backing source; fall back to a
9479        // placeholder of the tagged unit so rendering stays total.
9480        f.write_str("0")?;
9481        match self.unit {
9482            Some(SizeUnit::Kilo) => f.write_str("K"),
9483            Some(SizeUnit::Mega) => f.write_str("M"),
9484            Some(SizeUnit::Giga) => f.write_str("G"),
9485            None => Ok(()),
9486        }
9487    }
9488}
9489
9490impl Render for TablespaceOption {
9491    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9492        match self {
9493            Self::Size {
9494                kind, equals, size, ..
9495            } => {
9496                f.write_str(match kind {
9497                    TablespaceSizeOption::InitialSize => "INITIAL_SIZE",
9498                    TablespaceSizeOption::AutoextendSize => "AUTOEXTEND_SIZE",
9499                    TablespaceSizeOption::MaxSize => "MAX_SIZE",
9500                    TablespaceSizeOption::ExtentSize => "EXTENT_SIZE",
9501                    TablespaceSizeOption::UndoBufferSize => "UNDO_BUFFER_SIZE",
9502                    TablespaceSizeOption::RedoBufferSize => "REDO_BUFFER_SIZE",
9503                    TablespaceSizeOption::FileBlockSize => "FILE_BLOCK_SIZE",
9504                })?;
9505                f.write_str(if *equals { " = " } else { " " })?;
9506                size.render(ctx, f)
9507            }
9508            Self::Nodegroup { equals, value, .. } => {
9509                f.write_str("NODEGROUP")?;
9510                f.write_str(if *equals { " = " } else { " " })?;
9511                value.render(ctx, f)
9512            }
9513            Self::Engine {
9514                storage,
9515                equals,
9516                name,
9517                ..
9518            } => {
9519                f.write_str(if *storage { "STORAGE ENGINE" } else { "ENGINE" })?;
9520                f.write_str(if *equals { " = " } else { " " })?;
9521                name.render(ctx, f)
9522            }
9523            Self::Wait { negated, .. } => f.write_str(if *negated { "NO_WAIT" } else { "WAIT" }),
9524            Self::Comment { equals, value, .. } => {
9525                f.write_str("COMMENT")?;
9526                f.write_str(if *equals { " = " } else { " " })?;
9527                value.render(ctx, f)
9528            }
9529            Self::Encryption { equals, value, .. } => {
9530                f.write_str("ENCRYPTION")?;
9531                f.write_str(if *equals { " = " } else { " " })?;
9532                value.render(ctx, f)
9533            }
9534            Self::EngineAttribute { equals, value, .. } => {
9535                f.write_str("ENGINE_ATTRIBUTE")?;
9536                f.write_str(if *equals { " = " } else { " " })?;
9537                value.render(ctx, f)
9538            }
9539        }
9540    }
9541}
9542
9543/// Render a trailing option list, each option preceded by a single space (the canonical form
9544/// space-separates; the source `opt_comma` separators are not replayed). Shared by every
9545/// tablespace / logfile-group render.
9546fn render_tablespace_options(
9547    options: &[TablespaceOption],
9548    ctx: &RenderCtx<'_>,
9549    f: &mut fmt::Formatter<'_>,
9550) -> fmt::Result {
9551    for option in options {
9552        f.write_str(" ")?;
9553        option.render(ctx, f)?;
9554    }
9555    Ok(())
9556}
9557
9558impl Render for CreateTablespace {
9559    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9560        f.write_str(if self.undo {
9561            "CREATE UNDO TABLESPACE "
9562        } else {
9563            "CREATE TABLESPACE "
9564        })?;
9565        self.name.render(ctx, f)?;
9566        if let Some(datafile) = &self.datafile {
9567            f.write_str(" ADD DATAFILE ")?;
9568            datafile.render(ctx, f)?;
9569        }
9570        if let Some(lg) = &self.use_logfile_group {
9571            f.write_str(" USE LOGFILE GROUP ")?;
9572            lg.render(ctx, f)?;
9573        }
9574        render_tablespace_options(&self.options, ctx, f)
9575    }
9576}
9577
9578impl Render for AlterTablespace {
9579    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9580        // The `ALTER UNDO TABLESPACE` head is used exactly when the action is `SetState`.
9581        f.write_str(
9582            if matches!(self.action, AlterTablespaceAction::SetState { .. }) {
9583                "ALTER UNDO TABLESPACE "
9584            } else {
9585                "ALTER TABLESPACE "
9586            },
9587        )?;
9588        self.name.render(ctx, f)?;
9589        f.write_str(" ")?;
9590        self.action.render(ctx, f)
9591    }
9592}
9593
9594impl Render for AlterTablespaceAction {
9595    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9596        match self {
9597            Self::AddDatafile {
9598                datafile, options, ..
9599            } => {
9600                f.write_str("ADD DATAFILE ")?;
9601                datafile.render(ctx, f)?;
9602                render_tablespace_options(options, ctx, f)
9603            }
9604            Self::DropDatafile {
9605                datafile, options, ..
9606            } => {
9607                f.write_str("DROP DATAFILE ")?;
9608                datafile.render(ctx, f)?;
9609                render_tablespace_options(options, ctx, f)
9610            }
9611            Self::Rename { new_name, .. } => {
9612                f.write_str("RENAME TO ")?;
9613                new_name.render(ctx, f)
9614            }
9615            Self::Options { options, .. } => {
9616                // The bare option list is non-empty; render the first without a leading space,
9617                // the rest space-separated.
9618                let mut first = true;
9619                for option in options {
9620                    if !first {
9621                        f.write_str(" ")?;
9622                    }
9623                    first = false;
9624                    option.render(ctx, f)?;
9625                }
9626                Ok(())
9627            }
9628            Self::SetState { state, options, .. } => {
9629                f.write_str(match state {
9630                    UndoTablespaceState::Active => "SET ACTIVE",
9631                    UndoTablespaceState::Inactive => "SET INACTIVE",
9632                })?;
9633                render_tablespace_options(options, ctx, f)
9634            }
9635        }
9636    }
9637}
9638
9639impl Render for DropTablespace {
9640    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9641        f.write_str(if self.undo {
9642            "DROP UNDO TABLESPACE "
9643        } else {
9644            "DROP TABLESPACE "
9645        })?;
9646        self.name.render(ctx, f)?;
9647        render_tablespace_options(&self.options, ctx, f)
9648    }
9649}
9650
9651impl Render for CreateLogfileGroup {
9652    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9653        f.write_str("CREATE LOGFILE GROUP ")?;
9654        self.name.render(ctx, f)?;
9655        f.write_str(" ADD UNDOFILE ")?;
9656        self.undofile.render(ctx, f)?;
9657        render_tablespace_options(&self.options, ctx, f)
9658    }
9659}
9660
9661impl Render for AlterLogfileGroup {
9662    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9663        f.write_str("ALTER LOGFILE GROUP ")?;
9664        self.name.render(ctx, f)?;
9665        f.write_str(" ADD UNDOFILE ")?;
9666        self.undofile.render(ctx, f)?;
9667        render_tablespace_options(&self.options, ctx, f)
9668    }
9669}
9670
9671impl Render for DropLogfileGroup {
9672    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9673        f.write_str("DROP LOGFILE GROUP ")?;
9674        self.name.render(ctx, f)?;
9675        render_tablespace_options(&self.options, ctx, f)
9676    }
9677}
9678
9679impl<X: Extension + Render> Render for ObjectReference<X> {
9680    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9681        match self {
9682            Self::Named { kind, name, .. } => {
9683                kind.render(ctx, f)?;
9684                f.write_str(" ")?;
9685                name.render(ctx, f)
9686            }
9687            Self::Routine {
9688                kind, signature, ..
9689            } => {
9690                kind.render(ctx, f)?;
9691                f.write_str(" ")?;
9692                signature.render(ctx, f)
9693            }
9694            Self::Aggregate { name, args, .. } => {
9695                f.write_str("AGGREGATE ")?;
9696                name.render(ctx, f)?;
9697                args.render(ctx, f)
9698            }
9699            Self::Operator {
9700                schema, op, args, ..
9701            } => {
9702                f.write_str("OPERATOR ")?;
9703                for part in &schema.0 {
9704                    part.render(ctx, f)?;
9705                    f.write_str(".")?;
9706                }
9707                f.write_str(ctx.resolve(*op))?;
9708                f.write_str(" ")?;
9709                args.render(ctx, f)
9710            }
9711            Self::OperatorClass {
9712                family,
9713                name,
9714                access_method,
9715                ..
9716            } => {
9717                f.write_str(if *family {
9718                    "OPERATOR FAMILY "
9719                } else {
9720                    "OPERATOR CLASS "
9721                })?;
9722                name.render(ctx, f)?;
9723                f.write_str(" USING ")?;
9724                access_method.render(ctx, f)
9725            }
9726            Self::Cast { from, to, .. } => {
9727                f.write_str("CAST (")?;
9728                from.render(ctx, f)?;
9729                f.write_str(" AS ")?;
9730                to.render(ctx, f)?;
9731                f.write_str(")")
9732            }
9733            Self::Type { domain, name, .. } => {
9734                f.write_str(if *domain { "DOMAIN " } else { "TYPE " })?;
9735                name.render(ctx, f)
9736            }
9737            Self::Transform {
9738                type_name,
9739                language,
9740                ..
9741            } => {
9742                f.write_str("TRANSFORM FOR ")?;
9743                type_name.render(ctx, f)?;
9744                f.write_str(" LANGUAGE ")?;
9745                language.render(ctx, f)
9746            }
9747            Self::Trigger { name, table, .. } => {
9748                f.write_str("TRIGGER ")?;
9749                name.render(ctx, f)?;
9750                f.write_str(" ON ")?;
9751                table.render(ctx, f)
9752            }
9753        }
9754    }
9755}
9756
9757impl<X: Extension + Render> Render for AlterObjectDepends<X> {
9758    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9759        f.write_str("ALTER ")?;
9760        self.object.render(ctx, f)?;
9761        if self.no {
9762            f.write_str(" NO")?;
9763        }
9764        f.write_str(" DEPENDS ON EXTENSION ")?;
9765        self.extension.render(ctx, f)
9766    }
9767}
9768
9769impl<X: Extension + Render> Render for DropTransform<X> {
9770    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9771        // The `IF EXISTS` guard sits between `TRANSFORM` and `FOR`, so the transform
9772        // reference's `TRANSFORM FOR type LANGUAGE lang` render can't be emitted as one
9773        // unit — the parts are spelled out here around the guard. The object is always the
9774        // `Transform` variant (the parser builds no other for this node).
9775        let ObjectReference::Transform {
9776            type_name,
9777            language,
9778            ..
9779        } = &self.object
9780        else {
9781            unreachable!("DROP TRANSFORM always carries an ObjectReference::Transform")
9782        };
9783        f.write_str("DROP TRANSFORM")?;
9784        if self.if_exists {
9785            f.write_str(" IF EXISTS")?;
9786        }
9787        f.write_str(" FOR ")?;
9788        type_name.render(ctx, f)?;
9789        f.write_str(" LANGUAGE ")?;
9790        language.render(ctx, f)?;
9791        render_drop_behavior(self.behavior, ctx, f)
9792    }
9793}
9794
9795impl Render for ObjectRefKind {
9796    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9797        f.write_str(match self {
9798            Self::Table => "TABLE",
9799            Self::Sequence => "SEQUENCE",
9800            Self::View => "VIEW",
9801            Self::MaterializedView => "MATERIALIZED VIEW",
9802            Self::Index => "INDEX",
9803            Self::ForeignTable => "FOREIGN TABLE",
9804            Self::Collation => "COLLATION",
9805            Self::Conversion => "CONVERSION",
9806            Self::Statistics => "STATISTICS",
9807            Self::TextSearchParser => "TEXT SEARCH PARSER",
9808            Self::TextSearchDictionary => "TEXT SEARCH DICTIONARY",
9809            Self::TextSearchTemplate => "TEXT SEARCH TEMPLATE",
9810            Self::TextSearchConfiguration => "TEXT SEARCH CONFIGURATION",
9811            Self::AccessMethod => "ACCESS METHOD",
9812            Self::EventTrigger => "EVENT TRIGGER",
9813            Self::Extension => "EXTENSION",
9814            Self::ForeignDataWrapper => "FOREIGN DATA WRAPPER",
9815            // The `PROCEDURAL` prefix is exact-synonym sugar; the canonical render drops it.
9816            Self::Language => "LANGUAGE",
9817            Self::Publication => "PUBLICATION",
9818            Self::Schema => "SCHEMA",
9819            Self::Server => "SERVER",
9820            Self::Database => "DATABASE",
9821            Self::Role => "ROLE",
9822            Self::Tablespace => "TABLESPACE",
9823        })
9824    }
9825}
9826
9827impl<X: Extension + Render> Render for AggregateArgs<X> {
9828    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9829        match self {
9830            Self::Star { .. } => f.write_str("(*)"),
9831            Self::Types {
9832                direct, order_by, ..
9833            } => {
9834                f.write_str("(")?;
9835                render_comma_separated(direct, ctx, f)?;
9836                if let Some(order_by) = order_by {
9837                    if !direct.is_empty() {
9838                        f.write_str(" ")?;
9839                    }
9840                    f.write_str("ORDER BY ")?;
9841                    render_comma_separated(order_by, ctx, f)?;
9842                }
9843                f.write_str(")")
9844            }
9845        }
9846    }
9847}
9848
9849impl<X: Extension + Render> Render for OperatorArgs<X> {
9850    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9851        f.write_str("(")?;
9852        match &self.left {
9853            Some(left) => left.render(ctx, f)?,
9854            None => f.write_str("NONE")?,
9855        }
9856        f.write_str(", ")?;
9857        match &self.right {
9858            Some(right) => right.render(ctx, f)?,
9859            None => f.write_str("NONE")?,
9860        }
9861        f.write_str(")")
9862    }
9863}
9864
9865impl<X: Extension + Render> Render for CreateTypeDefinition<X> {
9866    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9867        match self {
9868            // Reuses the `ENUM(...)` value-list spelling of `DataType::Enum`; an empty label
9869            // list renders `ENUM()` (which DuckDB accepts).
9870            Self::Enum { labels, .. } => render_value_list_type(ctx, "ENUM", labels, f),
9871            Self::EnumFromQuery { query, .. } => {
9872                f.write_str("ENUM (")?;
9873                query.render(ctx, f)?;
9874                f.write_str(")")
9875            }
9876            Self::Alias { data_type, .. } => data_type.render(ctx, f),
9877        }
9878    }
9879}
9880
9881impl Render for ViewCheckOption {
9882    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9883        f.write_str(match self {
9884            Self::Unspecified => "WITH CHECK OPTION",
9885            Self::Cascaded => "WITH CASCADED CHECK OPTION",
9886            Self::Local => "WITH LOCAL CHECK OPTION",
9887        })
9888    }
9889}
9890
9891impl<X: Extension + Render> Render for CreateIndex<X> {
9892    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9893        f.write_str("CREATE")?;
9894        if self.unique {
9895            f.write_str(" UNIQUE")?;
9896        }
9897        f.write_str(" INDEX")?;
9898        if self.concurrently {
9899            f.write_str(" CONCURRENTLY")?;
9900        }
9901        if self.if_not_exists {
9902            f.write_str(" IF NOT EXISTS")?;
9903        }
9904        if let Some(name) = &self.name {
9905            f.write_str(" ")?;
9906            name.render(ctx, f)?;
9907        }
9908        f.write_str(" ON ")?;
9909        self.table.render(ctx, f)?;
9910        if let Some(using) = &self.using {
9911            f.write_str(" USING ")?;
9912            using.render(ctx, f)?;
9913        }
9914        f.write_str(" (")?;
9915        render_comma_separated(&self.columns, ctx, f)?;
9916        f.write_str(")")?;
9917        if !self.with_params.is_empty() {
9918            f.write_str(" WITH (")?;
9919            render_comma_separated(&self.with_params, ctx, f)?;
9920            f.write_str(")")?;
9921        }
9922        if let Some(predicate) = &self.predicate {
9923            f.write_str(" WHERE ")?;
9924            predicate.render(ctx, f)?;
9925        }
9926        Ok(())
9927    }
9928}
9929
9930/// Render the trailing ` ASC`/` DESC` then ` NULLS FIRST`/` NULLS LAST` sort
9931/// suffixes of a sort key — shared by `IndexColumn` and `OrderByExpr`. Each is a
9932/// leading-space clause omitted when its option is `None`.
9933fn render_sort_direction(
9934    asc: Option<bool>,
9935    nulls_first: Option<bool>,
9936    f: &mut fmt::Formatter<'_>,
9937) -> fmt::Result {
9938    match asc {
9939        Some(true) => f.write_str(" ASC")?,
9940        Some(false) => f.write_str(" DESC")?,
9941        None => {}
9942    }
9943    match nulls_first {
9944        Some(true) => f.write_str(" NULLS FIRST")?,
9945        Some(false) => f.write_str(" NULLS LAST")?,
9946        None => {}
9947    }
9948    Ok(())
9949}
9950
9951impl<X: Extension + Render> Render for IndexColumn<X> {
9952    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9953        self.expr.render(ctx, f)?;
9954        render_sort_direction(self.asc, self.nulls_first, f)
9955    }
9956}
9957
9958impl Render for CreateDatabase {
9959    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9960        f.write_str("CREATE DATABASE ")?;
9961        if self.if_not_exists {
9962            f.write_str("IF NOT EXISTS ")?;
9963        }
9964        self.name.render(ctx, f)
9965    }
9966}
9967
9968impl<X: Extension + Render> Render for CreateFunction<X> {
9969    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9970        f.write_str("CREATE ")?;
9971        if self.or_replace {
9972            f.write_str("OR REPLACE ")?;
9973        }
9974        if let Some(definer) = &self.definer {
9975            definer.render(ctx, f)?;
9976            f.write_str(" ")?;
9977        }
9978        f.write_str("FUNCTION ")?;
9979        if self.if_not_exists {
9980            f.write_str("IF NOT EXISTS ")?;
9981        }
9982        self.name.render(ctx, f)?;
9983        // The parameter list is always parenthesized, even when empty.
9984        f.write_str("(")?;
9985        render_comma_separated(&self.params, ctx, f)?;
9986        f.write_str(")")?;
9987        if let Some(returns) = &self.returns {
9988            f.write_str(" RETURNS ")?;
9989            returns.render(ctx, f)?;
9990        }
9991        for option in &self.options {
9992            f.write_str(" ")?;
9993            option.render(ctx, f)?;
9994        }
9995        // The trailing SQL-standard routine body (`RETURN <expr>`) follows the whole option
9996        // list, its written source position (proven by oracle: it never precedes an option).
9997        if let Some(body) = &self.body {
9998            f.write_str(" ")?;
9999            body.render(ctx, f)?;
10000        }
10001        Ok(())
10002    }
10003}
10004
10005impl<X: Extension + Render> Render for FunctionParam<X> {
10006    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10007        if let Some(mode) = &self.mode {
10008            f.write_str(match mode {
10009                FunctionParamMode::In => "IN ",
10010                FunctionParamMode::Out => "OUT ",
10011                FunctionParamMode::InOut => "INOUT ",
10012                FunctionParamMode::Variadic => "VARIADIC ",
10013            })?;
10014        }
10015        if let Some(name) = &self.name {
10016            name.render(ctx, f)?;
10017            f.write_str(" ")?;
10018        }
10019        self.data_type.render(ctx, f)?;
10020        if let Some(default) = &self.default {
10021            f.write_str(" ")?;
10022            default.render(ctx, f)?;
10023        }
10024        Ok(())
10025    }
10026}
10027
10028impl<X: Extension + Render> Render for FunctionParamDefault<X> {
10029    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10030        f.write_str(match self.spelling {
10031            FunctionParamDefaultSpelling::Default => "DEFAULT ",
10032            FunctionParamDefaultSpelling::Equals => "= ",
10033        })?;
10034        self.value.render(ctx, f)
10035    }
10036}
10037
10038impl<X: Extension + Render> Render for CreateMacro<X> {
10039    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10040        f.write_str("CREATE")?;
10041        if self.or_replace {
10042            f.write_str(" OR REPLACE")?;
10043        }
10044        if let Some(temporary) = self.temporary {
10045            f.write_str(" ")?;
10046            temporary.render(ctx, f)?;
10047        }
10048        f.write_str(match self.spelling {
10049            MacroSpelling::Macro => " MACRO",
10050            MacroSpelling::Function => " FUNCTION",
10051        })?;
10052        if self.if_not_exists {
10053            f.write_str(" IF NOT EXISTS")?;
10054        }
10055        f.write_str(" ")?;
10056        self.name.render(ctx, f)?;
10057        // The parameter list is always parenthesized, even when empty.
10058        f.write_str("(")?;
10059        render_comma_separated(&self.params, ctx, f)?;
10060        f.write_str(") AS ")?;
10061        self.body.render(ctx, f)
10062    }
10063}
10064
10065impl<X: Extension + Render> Render for MacroParam<X> {
10066    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10067        self.name.render(ctx, f)?;
10068        if let Some(default) = &self.default {
10069            f.write_str(" := ")?;
10070            default.render(ctx, f)?;
10071        }
10072        Ok(())
10073    }
10074}
10075
10076impl<X: Extension + Render> Render for MacroBody<X> {
10077    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10078        match self {
10079            Self::Scalar { expr, .. } => expr.render(ctx, f),
10080            Self::Table { query, .. } => {
10081                f.write_str("TABLE ")?;
10082                query.render(ctx, f)
10083            }
10084        }
10085    }
10086}
10087
10088impl<X: Extension + Render> Render for FunctionOption<X> {
10089    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10090        match self {
10091            Self::Language { name, .. } => {
10092                f.write_str("LANGUAGE ")?;
10093                name.render(ctx, f)
10094            }
10095            Self::As { body, .. } => {
10096                f.write_str("AS ")?;
10097                body.render(ctx, f)
10098            }
10099            Self::NullBehavior { behavior, .. } => behavior.render(ctx, f),
10100            Self::Deterministic { not, .. } => f.write_str(if *not {
10101                "NOT DETERMINISTIC"
10102            } else {
10103                "DETERMINISTIC"
10104            }),
10105            Self::DataAccess { access, .. } => access.render(ctx, f),
10106            Self::SqlSecurity { context, .. } => {
10107                f.write_str("SQL SECURITY ")?;
10108                context.render(ctx, f)
10109            }
10110            Self::Comment { comment, .. } => {
10111                f.write_str("COMMENT ")?;
10112                comment.render(ctx, f)
10113            }
10114        }
10115    }
10116}
10117
10118impl Render for SqlDataAccess {
10119    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10120        f.write_str(match self {
10121            Self::ContainsSql => "CONTAINS SQL",
10122            Self::NoSql => "NO SQL",
10123            Self::ReadsSqlData => "READS SQL DATA",
10124            Self::ModifiesSqlData => "MODIFIES SQL DATA",
10125        })
10126    }
10127}
10128
10129impl Render for SqlSecurityContext {
10130    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10131        f.write_str(match self {
10132            Self::Definer => "DEFINER",
10133            Self::Invoker => "INVOKER",
10134        })
10135    }
10136}
10137
10138impl<X: Extension + Render> Render for FunctionBody<X> {
10139    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10140        match self {
10141            // The body [`Literal`] renders from its source span, so a dollar-quoted body
10142            // (`$tag$…$tag$`) reproduces its delimiters and verbatim text exactly.
10143            Self::Definition { definition, .. } => definition.render(ctx, f),
10144            // The SQL-standard live body: the `RETURN` keyword then the expression.
10145            Self::Return { expr, .. } => {
10146                f.write_str("RETURN ")?;
10147                expr.render(ctx, f)
10148            }
10149            // The MySQL SQL/PSM body statement (usually a `BEGIN … END` compound block).
10150            Self::Block { body, .. } => body.render(ctx, f),
10151        }
10152    }
10153}
10154
10155impl Render for Definer {
10156    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10157        f.write_str("DEFINER = ")?;
10158        match self {
10159            Self::Account { user, host, .. } => {
10160                user.render(ctx, f)?;
10161                if let Some(host) = host {
10162                    f.write_str("@")?;
10163                    host.render(ctx, f)?;
10164                }
10165                Ok(())
10166            }
10167            Self::CurrentUser { parens, .. } => {
10168                f.write_str("CURRENT_USER")?;
10169                if *parens {
10170                    f.write_str("()")?;
10171                }
10172                Ok(())
10173            }
10174        }
10175    }
10176}
10177
10178impl<X: Extension + Render> Render for CreateProcedure<X> {
10179    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10180        f.write_str("CREATE ")?;
10181        if let Some(definer) = &self.definer {
10182            definer.render(ctx, f)?;
10183            f.write_str(" ")?;
10184        }
10185        f.write_str("PROCEDURE ")?;
10186        if self.if_not_exists {
10187            f.write_str("IF NOT EXISTS ")?;
10188        }
10189        self.name.render(ctx, f)?;
10190        // The parameter list is always parenthesized, even when empty.
10191        f.write_str("(")?;
10192        render_comma_separated(&self.params, ctx, f)?;
10193        f.write_str(")")?;
10194        for characteristic in &self.characteristics {
10195            f.write_str(" ")?;
10196            characteristic.render(ctx, f)?;
10197        }
10198        f.write_str(" ")?;
10199        self.body.render(ctx, f)
10200    }
10201}
10202
10203impl<X: Extension + Render> Render for AlterRoutine<X> {
10204    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10205        f.write_str(match self.kind {
10206            RoutineKind::Procedure => "ALTER PROCEDURE ",
10207            RoutineKind::Function => "ALTER FUNCTION ",
10208        })?;
10209        self.name.render(ctx, f)?;
10210        for characteristic in &self.characteristics {
10211            f.write_str(" ")?;
10212            characteristic.render(ctx, f)?;
10213        }
10214        Ok(())
10215    }
10216}
10217
10218impl Render for FunctionNullBehavior {
10219    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10220        f.write_str(match self {
10221            Self::CalledOnNull => "CALLED ON NULL INPUT",
10222            Self::ReturnsNullOnNull => "RETURNS NULL ON NULL INPUT",
10223            Self::Strict => "STRICT",
10224        })
10225    }
10226}
10227
10228impl<X: Extension + Render> Render for EventSchedule<X> {
10229    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10230        match self {
10231            Self::At { at, .. } => {
10232                f.write_str("AT ")?;
10233                at.render(ctx, f)
10234            }
10235            Self::Every {
10236                value,
10237                unit,
10238                starts,
10239                ends,
10240                ..
10241            } => {
10242                f.write_str("EVERY ")?;
10243                value.render(ctx, f)?;
10244                // The unit reuses the shared IntervalFields vocabulary in MySQL underscore
10245                // spelling; the suffix carries its own leading space.
10246                f.write_str(mysql_interval_unit_suffix(*unit))?;
10247                if let Some(starts) = starts {
10248                    f.write_str(" STARTS ")?;
10249                    starts.render(ctx, f)?;
10250                }
10251                if let Some(ends) = ends {
10252                    f.write_str(" ENDS ")?;
10253                    ends.render(ctx, f)?;
10254                }
10255                Ok(())
10256            }
10257        }
10258    }
10259}
10260
10261/// Render the ` ON COMPLETION [NOT] PRESERVE` clause (leading space) shared by
10262/// `CREATE`/`ALTER EVENT`.
10263fn render_event_on_completion(
10264    on_completion: EventOnCompletion,
10265    f: &mut fmt::Formatter<'_>,
10266) -> fmt::Result {
10267    f.write_str(match on_completion {
10268        EventOnCompletion::Preserve => " ON COMPLETION PRESERVE",
10269        EventOnCompletion::NotPreserve => " ON COMPLETION NOT PRESERVE",
10270    })
10271}
10272
10273/// Render the ` ENABLE | DISABLE [ON SLAVE|REPLICA]` status clause (leading space).
10274fn render_event_status(status: EventStatus, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10275    f.write_str(match status {
10276        EventStatus::Enable => " ENABLE",
10277        EventStatus::Disable => " DISABLE",
10278        EventStatus::DisableOnReplica(ReplicaSpelling::Slave) => " DISABLE ON SLAVE",
10279        EventStatus::DisableOnReplica(ReplicaSpelling::Replica) => " DISABLE ON REPLICA",
10280    })
10281}
10282
10283impl<X: Extension + Render> Render for CreateEvent<X> {
10284    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10285        f.write_str("CREATE ")?;
10286        if let Some(definer) = &self.definer {
10287            definer.render(ctx, f)?;
10288            f.write_str(" ")?;
10289        }
10290        f.write_str("EVENT ")?;
10291        if self.if_not_exists {
10292            f.write_str("IF NOT EXISTS ")?;
10293        }
10294        self.name.render(ctx, f)?;
10295        f.write_str(" ON SCHEDULE ")?;
10296        self.schedule.render(ctx, f)?;
10297        if let Some(on_completion) = self.on_completion {
10298            render_event_on_completion(on_completion, f)?;
10299        }
10300        if let Some(status) = self.status {
10301            render_event_status(status, f)?;
10302        }
10303        if let Some(comment) = &self.comment {
10304            f.write_str(" COMMENT ")?;
10305            comment.render(ctx, f)?;
10306        }
10307        f.write_str(" DO ")?;
10308        self.body.render(ctx, f)
10309    }
10310}
10311
10312impl<X: Extension + Render> Render for AlterEvent<X> {
10313    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10314        f.write_str("ALTER ")?;
10315        if let Some(definer) = &self.definer {
10316            definer.render(ctx, f)?;
10317            f.write_str(" ")?;
10318        }
10319        f.write_str("EVENT ")?;
10320        self.name.render(ctx, f)?;
10321        if let Some(schedule) = &self.schedule {
10322            f.write_str(" ON SCHEDULE ")?;
10323            schedule.render(ctx, f)?;
10324        }
10325        if let Some(on_completion) = self.on_completion {
10326            render_event_on_completion(on_completion, f)?;
10327        }
10328        if let Some(rename_to) = &self.rename_to {
10329            f.write_str(" RENAME TO ")?;
10330            rename_to.render(ctx, f)?;
10331        }
10332        if let Some(status) = self.status {
10333            render_event_status(status, f)?;
10334        }
10335        if let Some(comment) = &self.comment {
10336            f.write_str(" COMMENT ")?;
10337            comment.render(ctx, f)?;
10338        }
10339        if let Some(body) = &self.body {
10340            f.write_str(" DO ")?;
10341            body.render(ctx, f)?;
10342        }
10343        Ok(())
10344    }
10345}
10346
10347impl Render for DropEvent {
10348    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10349        f.write_str("DROP EVENT ")?;
10350        if self.if_exists {
10351            f.write_str("IF EXISTS ")?;
10352        }
10353        self.name.render(ctx, f)
10354    }
10355}
10356
10357impl Render for DropDatabase {
10358    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10359        f.write_str(match self.spelling {
10360            DatabaseKeyword::Database => "DROP DATABASE ",
10361            DatabaseKeyword::Schema => "DROP SCHEMA ",
10362        })?;
10363        if self.if_exists {
10364            f.write_str("IF EXISTS ")?;
10365        }
10366        self.name.render(ctx, f)
10367    }
10368}
10369
10370impl Render for DropIndexOnTable {
10371    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10372        f.write_str("DROP INDEX ")?;
10373        self.name.render(ctx, f)?;
10374        f.write_str(" ON ")?;
10375        self.table.render(ctx, f)?;
10376        for option in &self.options {
10377            f.write_str(" ")?;
10378            option.render(ctx, f)?;
10379        }
10380        Ok(())
10381    }
10382}
10383
10384impl Render for IndexLockAlgorithmOption {
10385    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10386        let (keyword, equals, value) = match self {
10387            Self::Algorithm { equals, value } => ("ALGORITHM", *equals, value.render_str()),
10388            Self::Lock { equals, value } => ("LOCK", *equals, value.render_str()),
10389        };
10390        f.write_str(keyword)?;
10391        f.write_str(if equals { " = " } else { " " })?;
10392        f.write_str(value)
10393    }
10394}
10395
10396impl IndexAlgorithm {
10397    /// The canonical keyword spelling of this algorithm value.
10398    fn render_str(self) -> &'static str {
10399        match self {
10400            Self::Default => "DEFAULT",
10401            Self::Inplace => "INPLACE",
10402            Self::Instant => "INSTANT",
10403            Self::Copy => "COPY",
10404        }
10405    }
10406}
10407
10408impl IndexLock {
10409    /// The canonical keyword spelling of this lock value.
10410    fn render_str(self) -> &'static str {
10411        match self {
10412            Self::Default => "DEFAULT",
10413            Self::None => "NONE",
10414            Self::Shared => "SHARED",
10415            Self::Exclusive => "EXCLUSIVE",
10416        }
10417    }
10418}
10419
10420impl<X: Extension + Render> Render for Query<X> {
10421    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10422        if let Some(with) = &self.with {
10423            with.render(ctx, f)?;
10424            f.write_str(" ")?;
10425        }
10426        self.body.render(ctx, f)?;
10427        if !self.order_by.is_empty() {
10428            f.write_str(" ORDER BY ")?;
10429            render_comma_separated(&self.order_by, ctx, f)?;
10430        }
10431        // DuckDB's `ORDER BY ALL` mode; the parser never populates it alongside
10432        // `order_by` (the engine rejects mixing), so the two arms cannot both fire.
10433        if let Some(all) = &self.order_by_all {
10434            f.write_str(" ORDER BY ")?;
10435            all.render(ctx, f)?;
10436        }
10437        // ClickHouse `LIMIT n [OFFSET m] BY …` precedes the ordinary `LIMIT`; a query
10438        // may carry both, so this is emitted before, not instead of, the `limit` tail.
10439        if let Some(limit_by) = &self.limit_by {
10440            f.write_str(" ")?;
10441            limit_by.render(ctx, f)?;
10442        }
10443        if let Some(limit) = &self.limit {
10444            f.write_str(" ")?;
10445            limit.render(ctx, f)?;
10446        }
10447        // ClickHouse `SETTINGS name = value, …` follows the ordinary `LIMIT` tail.
10448        if !self.settings.is_empty() {
10449            f.write_str(" SETTINGS ")?;
10450            render_comma_separated(&self.settings, ctx, f)?;
10451        }
10452        // ClickHouse `FORMAT <name>` closes the query, the last tail after `SETTINGS`.
10453        if let Some(format) = &self.format {
10454            f.write_str(" ")?;
10455            format.render(ctx, f)?;
10456        }
10457        // Row-locking clauses trail the whole query, after LIMIT (MySQL's fixed
10458        // position; PostgreSQL also accepts them here). Space-separated when stacked.
10459        for locking in &self.locking {
10460            f.write_str(" ")?;
10461            locking.render(ctx, f)?;
10462        }
10463        // BigQuery/ZetaSQL `|>` pipe operators trail everything else, one ` |> …` step per
10464        // element in written order. Empty for every shipped preset (the gate is off).
10465        for op in &self.pipe_operators {
10466            f.write_str(" ")?;
10467            op.render(ctx, f)?;
10468        }
10469        // MSSQL `FOR XML`/`FOR JSON` result-shaping tail closes the query, after every
10470        // other clause. `None` unless the MSSQL/Lenient gate is on.
10471        if let Some(for_clause) = &self.for_clause {
10472            f.write_str(" ")?;
10473            for_clause.render(ctx, f)?;
10474        }
10475        Ok(())
10476    }
10477}
10478
10479impl<X: Extension + Render> Render for PipeOperator<X> {
10480    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10481        // The framework seam (render side): one arm per operator, each emitting
10482        // `|> <KEYWORD> …`. The leading ` ` and the whole-list loop live on `Query`'s
10483        // render, so an arm writes only its own `|> …`.
10484        match self {
10485            PipeOperator::Where { predicate, .. } => {
10486                f.write_str("|> WHERE ")?;
10487                predicate.render(ctx, f)
10488            }
10489            PipeOperator::Select { items, .. } => {
10490                f.write_str("|> SELECT ")?;
10491                render_comma_separated(items, ctx, f)
10492            }
10493            PipeOperator::Extend { items, .. } => {
10494                f.write_str("|> EXTEND ")?;
10495                render_comma_separated(items, ctx, f)
10496            }
10497            PipeOperator::As { alias, .. } => {
10498                f.write_str("|> AS ")?;
10499                alias.render(ctx, f)
10500            }
10501            PipeOperator::OrderBy { keys, .. } => {
10502                f.write_str("|> ORDER BY ")?;
10503                render_comma_separated(keys, ctx, f)
10504            }
10505            PipeOperator::Limit { count, offset, .. } => {
10506                f.write_str("|> LIMIT ")?;
10507                count.render(ctx, f)?;
10508                if let Some(offset) = offset {
10509                    f.write_str(" OFFSET ")?;
10510                    offset.render(ctx, f)?;
10511                }
10512                Ok(())
10513            }
10514            PipeOperator::Join { join, .. } => {
10515                // `Join` renders `<keyword> <relation> <constraint>`, so `|> ` + the join is
10516                // `|> [<type>] JOIN <relation> [ON | USING]`.
10517                f.write_str("|> ")?;
10518                join.render(ctx, f)
10519            }
10520            PipeOperator::SetOperation {
10521                op,
10522                quantifier,
10523                queries,
10524                ..
10525            } => {
10526                f.write_str("|> ")?;
10527                op.render(ctx, f)?;
10528                if let Some(quantifier) = quantifier {
10529                    f.write_str(" ")?;
10530                    quantifier.render(ctx, f)?;
10531                }
10532                f.write_str(" ")?;
10533                // Each operand is a parenthesized query; `Query`'s render emits no
10534                // surrounding parentheses, so they are written here.
10535                for (index, query) in queries.iter().enumerate() {
10536                    if index > 0 {
10537                        f.write_str(", ")?;
10538                    }
10539                    f.write_str("(")?;
10540                    query.render(ctx, f)?;
10541                    f.write_str(")")?;
10542                }
10543                Ok(())
10544            }
10545            PipeOperator::Set { assignments, .. } => {
10546                f.write_str("|> SET ")?;
10547                render_comma_separated(assignments, ctx, f)
10548            }
10549            PipeOperator::Call { call, alias, .. } => {
10550                f.write_str("|> CALL ")?;
10551                call.render(ctx, f)?;
10552                if let Some(alias) = alias {
10553                    f.write_str(" AS ")?;
10554                    alias.render(ctx, f)?;
10555                }
10556                Ok(())
10557            }
10558            PipeOperator::Aggregate {
10559                aggregates,
10560                group_by,
10561                ..
10562            } => {
10563                // The aggregate list is empty for a grouping-only operator, so the
10564                // separating space is written only when there is a list to follow it.
10565                f.write_str("|> AGGREGATE")?;
10566                if !aggregates.is_empty() {
10567                    f.write_str(" ")?;
10568                    render_comma_separated(aggregates, ctx, f)?;
10569                }
10570                if !group_by.is_empty() {
10571                    f.write_str(" GROUP BY ")?;
10572                    render_comma_separated(group_by, ctx, f)?;
10573                }
10574                Ok(())
10575            }
10576            PipeOperator::Drop { columns, .. } => {
10577                f.write_str("|> DROP ")?;
10578                render_comma_separated(columns, ctx, f)
10579            }
10580            PipeOperator::Rename { renames, .. } => {
10581                f.write_str("|> RENAME ")?;
10582                render_comma_separated(renames, ctx, f)
10583            }
10584            PipeOperator::Pivot {
10585                aggregates, column, ..
10586            } => {
10587                f.write_str("|> PIVOT (")?;
10588                render_comma_separated(aggregates, ctx, f)?;
10589                f.write_str(" FOR ")?;
10590                // `PivotColumn` renders `<col> IN (<values>)`.
10591                column.render(ctx, f)?;
10592                f.write_str(")")
10593            }
10594            PipeOperator::Unpivot {
10595                value,
10596                name,
10597                columns,
10598                ..
10599            } => {
10600                f.write_str("|> UNPIVOT (")?;
10601                render_unpivot_name_list(value, ctx, f)?;
10602                f.write_str(" FOR ")?;
10603                render_unpivot_name_list(name, ctx, f)?;
10604                f.write_str(" IN (")?;
10605                render_comma_separated(columns, ctx, f)?;
10606                f.write_str("))")
10607            }
10608            PipeOperator::TableSample { sample, .. } => {
10609                // `TableSample`'s render carries its own leading ` TABLESAMPLE ` (it is a
10610                // `FROM`-suffix node), so the `|>` is written bare against it.
10611                f.write_str("|>")?;
10612                sample.render(ctx, f)
10613            }
10614        }
10615    }
10616}
10617
10618impl<X: Extension + Render> Render for SemiStructuredAccessExpr<X> {
10619    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10620        let full = ctx.mode() == RenderMode::Parenthesized;
10621        open_group(full, f)?;
10622        render_operand(
10623            &self.base,
10624            operand_needs_parens(
10625                &ctx.target().binding_powers,
10626                ctx.target().binding_powers.subscript,
10627                &self.base,
10628                Side::Left,
10629            ),
10630            ctx,
10631            f,
10632        )?;
10633        let mut segments = self.path.iter();
10634        if let Some(first) = segments.next() {
10635            f.write_str(":")?;
10636            render_semi_structured_path_segment(first, false, ctx, f)?;
10637        }
10638        for segment in segments {
10639            render_semi_structured_path_segment(segment, true, ctx, f)?;
10640        }
10641        close_group(full, f)
10642    }
10643}
10644
10645fn render_semi_structured_path_segment<X: Extension + Render>(
10646    segment: &SemiStructuredPathSegment<X>,
10647    suffix: bool,
10648    ctx: &RenderCtx<'_>,
10649    f: &mut fmt::Formatter<'_>,
10650) -> fmt::Result {
10651    match segment {
10652        SemiStructuredPathSegment::Key { key, .. } => {
10653            if suffix {
10654                f.write_str(".")?;
10655            }
10656            key.render(ctx, f)
10657        }
10658        SemiStructuredPathSegment::Index { index, .. } => {
10659            f.write_str("[")?;
10660            index.render(ctx, f)?;
10661            f.write_str("]")
10662        }
10663    }
10664}
10665
10666impl<X: Extension + Render> Render for PipeAggregateExpr<X> {
10667    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10668        self.expr.render(ctx, f)?;
10669        if let Some(alias) = &self.alias {
10670            f.write_str(" AS ")?;
10671            alias.render(ctx, f)?;
10672        }
10673        render_sort_direction(self.asc, self.nulls_first, f)
10674    }
10675}
10676
10677impl Render for PipeRenameItem {
10678    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10679        self.old.render(ctx, f)?;
10680        f.write_str(" AS ")?;
10681        self.new.render(ctx, f)
10682    }
10683}
10684
10685impl Render for LockingClause {
10686    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10687        match self.spelling {
10688            // MySQL's legacy spelling of `FOR SHARE`, a bare clause with no `OF`/wait
10689            // tail (the parser only builds it on `LockStrength::Share`).
10690            LockingSpelling::LockInShareMode => return f.write_str("LOCK IN SHARE MODE"),
10691            LockingSpelling::Modern => {}
10692        }
10693        f.write_str(match self.strength {
10694            LockStrength::Update => "FOR UPDATE",
10695            LockStrength::NoKeyUpdate => "FOR NO KEY UPDATE",
10696            LockStrength::Share => "FOR SHARE",
10697            LockStrength::KeyShare => "FOR KEY SHARE",
10698        })?;
10699        if !self.of.is_empty() {
10700            f.write_str(" OF ")?;
10701            render_comma_separated(&self.of, ctx, f)?;
10702        }
10703        match self.wait {
10704            Some(LockWait::NoWait) => f.write_str(" NOWAIT")?,
10705            Some(LockWait::SkipLocked) => f.write_str(" SKIP LOCKED")?,
10706            None => {}
10707        }
10708        Ok(())
10709    }
10710}
10711
10712impl Render for IndexHint {
10713    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10714        f.write_str(match self.action {
10715            IndexHintAction::Use => "USE ",
10716            IndexHintAction::Ignore => "IGNORE ",
10717            IndexHintAction::Force => "FORCE ",
10718        })?;
10719        f.write_str(match self.keyword {
10720            IndexHintKeyword::Index => "INDEX",
10721            IndexHintKeyword::Key => "KEY",
10722        })?;
10723        if let Some(scope) = self.scope {
10724            f.write_str(match scope {
10725                IndexHintScope::Join => " FOR JOIN",
10726                IndexHintScope::OrderBy => " FOR ORDER BY",
10727                IndexHintScope::GroupBy => " FOR GROUP BY",
10728            })?;
10729        }
10730        // The parenthesized index list is mandatory syntax even when empty
10731        // (`USE INDEX ()`), so the parens are always written.
10732        f.write_str(" (")?;
10733        render_comma_separated(&self.indexes, ctx, f)?;
10734        f.write_str(")")
10735    }
10736}
10737
10738impl Render for IndexedBy {
10739    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10740        match self {
10741            IndexedBy::Named { index, .. } => {
10742                f.write_str("INDEXED BY ")?;
10743                index.render(ctx, f)
10744            }
10745            IndexedBy::NotIndexed { .. } => f.write_str("NOT INDEXED"),
10746        }
10747    }
10748}
10749
10750impl Render for TableHint {
10751    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10752        match self {
10753            TableHint::Keyword { keyword, .. } => f.write_str(keyword.as_str()),
10754            // `INDEX = <index>` for a single index under the `=` spelling; otherwise the
10755            // parenthesized list (`INDEX (a, b)` / `INDEX = (a, b)`).
10756            TableHint::Index {
10757                equals, indexes, ..
10758            } => {
10759                if *equals && indexes.len() == 1 {
10760                    f.write_str("INDEX = ")?;
10761                    indexes[0].render(ctx, f)
10762                } else {
10763                    f.write_str(if *equals { "INDEX = (" } else { "INDEX (" })?;
10764                    render_comma_separated(indexes, ctx, f)?;
10765                    f.write_str(")")
10766                }
10767            }
10768            TableHint::ForceSeek { target: None, .. } => f.write_str("FORCESEEK"),
10769            TableHint::ForceSeek {
10770                target: Some(target),
10771                ..
10772            } => {
10773                f.write_str("FORCESEEK (")?;
10774                target.index.render(ctx, f)?;
10775                f.write_str(" (")?;
10776                render_comma_separated(&target.columns, ctx, f)?;
10777                f.write_str("))")
10778            }
10779            TableHint::Other { ident, .. } => ident.render(ctx, f),
10780        }
10781    }
10782}
10783
10784impl<X: Extension + Render> Render for TableVersion<X> {
10785    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10786        match self {
10787            TableVersion::ForSystemTimeAsOf { point, .. } => {
10788                f.write_str("FOR SYSTEM_TIME AS OF ")?;
10789                point.render(ctx, f)
10790            }
10791            TableVersion::ForSystemTimeFromTo { start, end, .. } => {
10792                f.write_str("FOR SYSTEM_TIME FROM ")?;
10793                start.render(ctx, f)?;
10794                f.write_str(" TO ")?;
10795                end.render(ctx, f)
10796            }
10797            TableVersion::ForSystemTimeBetween { start, end, .. } => {
10798                f.write_str("FOR SYSTEM_TIME BETWEEN ")?;
10799                start.render(ctx, f)?;
10800                f.write_str(" AND ")?;
10801                end.render(ctx, f)
10802            }
10803            TableVersion::ForSystemTimeContainedIn { start, end, .. } => {
10804                f.write_str("FOR SYSTEM_TIME CONTAINED IN (")?;
10805                start.render(ctx, f)?;
10806                f.write_str(", ")?;
10807                end.render(ctx, f)?;
10808                f.write_str(")")
10809            }
10810            TableVersion::ForSystemTimeAll { .. } => f.write_str("FOR SYSTEM_TIME ALL"),
10811            TableVersion::VersionAsOf { version, .. } => {
10812                f.write_str("VERSION AS OF ")?;
10813                version.render(ctx, f)
10814            }
10815            TableVersion::TimestampAsOf { point, .. } => {
10816                f.write_str("TIMESTAMP AS OF ")?;
10817                point.render(ctx, f)
10818            }
10819        }
10820    }
10821}
10822
10823impl<X: Extension + Render> Render for SetExpr<X> {
10824    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10825        match self {
10826            SetExpr::Select { select, .. } => select.render(ctx, f),
10827            SetExpr::Values { values, .. } => values.render(ctx, f),
10828            // A nested query body carries its own ORDER BY / LIMIT, so it is
10829            // parenthesized to stay unambiguous inside an enclosing query.
10830            SetExpr::Query { query, .. } => {
10831                f.write_str("(")?;
10832                query.render(ctx, f)?;
10833                f.write_str(")")
10834            }
10835            SetExpr::SetOperation {
10836                op,
10837                all,
10838                by_name,
10839                left,
10840                right,
10841                ..
10842            } => {
10843                render_set_operand(op, left, Side::Left, ctx, f)?;
10844                f.write_str(" ")?;
10845                op.render(ctx, f)?;
10846                if *all {
10847                    f.write_str(" ALL")?;
10848                }
10849                // DuckDB's name-matched `UNION [ALL] BY NAME`, written after the
10850                // optional `ALL` (`UNION ALL BY NAME`; probed on 1.5.4).
10851                if *by_name {
10852                    f.write_str(" BY NAME")?;
10853                }
10854                f.write_str(" ")?;
10855                render_set_operand(op, right, Side::Right, ctx, f)
10856            }
10857            // A statement-spelled PIVOT/UNPIVOT standing as a query body renders bare;
10858            // the enclosing position (a CTE's `( … )`, a `CREATE VIEW … AS`) supplies
10859            // any parentheses, exactly as `SetExpr::Select` does.
10860            SetExpr::Pivot { pivot, .. } => pivot.render(ctx, f),
10861            SetExpr::Unpivot { unpivot, .. } => unpivot.render(ctx, f),
10862        }
10863    }
10864}
10865
10866/// Render a child set expression under a set-operation parent.
10867///
10868/// In `Parenthesized` mode every nested `SetOperation` operand is wrapped; the
10869/// root query body stays unwrapped so a rendered statement still starts with its
10870/// query keyword. Canonical/redacted rendering adds only the parentheses needed
10871/// to preserve grouping under the target dialect's set-operation table.
10872fn render_set_operand<X: Extension + Render>(
10873    parent: &SetOperator,
10874    child: &SetExpr<X>,
10875    side: Side,
10876    ctx: &RenderCtx<'_>,
10877    f: &mut fmt::Formatter<'_>,
10878) -> fmt::Result {
10879    let wrap = match ctx.mode() {
10880        RenderMode::Parenthesized => matches!(child, SetExpr::SetOperation { .. }),
10881        RenderMode::Canonical | RenderMode::Redacted => {
10882            set_child_needs_parens(&ctx.target().set_operation_powers, parent, child, side)
10883        }
10884    };
10885    open_group(wrap, f)?;
10886    child.render(ctx, f)?;
10887    close_group(wrap, f)
10888}
10889
10890/// Whether a set-operation parent's child needs canonical parentheses.
10891fn set_child_needs_parens<X: Extension>(
10892    bp: &SetOperationBindingPowerTable,
10893    parent: &SetOperator,
10894    child: &SetExpr<X>,
10895    side: Side,
10896) -> bool {
10897    match child {
10898        SetExpr::SetOperation { op, .. } => bp.needs_parens(parent, op, side),
10899        _ => false,
10900    }
10901}
10902
10903impl<X: Extension + Render> Render for With<X> {
10904    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10905        f.write_str("WITH")?;
10906        if self.recursive {
10907            f.write_str(" RECURSIVE")?;
10908        }
10909        render_leading_space_comma_separated(&self.ctes, ctx, f)
10910    }
10911}
10912
10913impl<X: Extension + Render> Render for Cte<X> {
10914    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10915        self.name.render(ctx, f)?;
10916        if !self.columns.is_empty() {
10917            f.write_str("(")?;
10918            render_ident_list(&self.columns, ctx, f)?;
10919            f.write_str(")")?;
10920        }
10921        // DuckDB's `USING KEY (cols)` key clause sits between the column list and `AS`.
10922        if let Some(key_columns) = &self.using_key {
10923            f.write_str(" USING KEY (")?;
10924            render_ident_list(key_columns, ctx, f)?;
10925            f.write_str(")")?;
10926        }
10927        f.write_str(" AS")?;
10928        match self.materialized {
10929            Some(true) => f.write_str(" MATERIALIZED")?,
10930            Some(false) => f.write_str(" NOT MATERIALIZED")?,
10931            None => {}
10932        }
10933        f.write_str(" (")?;
10934        self.body.render(ctx, f)?;
10935        f.write_str(")")?;
10936        // The SQL:2023 recursive-query clauses trail the body's `)`, SEARCH before CYCLE.
10937        if let Some(search) = &self.search {
10938            f.write_str(" ")?;
10939            search.render(ctx, f)?;
10940        }
10941        if let Some(cycle) = &self.cycle {
10942            f.write_str(" ")?;
10943            cycle.render(ctx, f)?;
10944        }
10945        Ok(())
10946    }
10947}
10948
10949impl Render for CteSearchClause {
10950    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10951        f.write_str(if self.breadth_first {
10952            "SEARCH BREADTH FIRST BY "
10953        } else {
10954            "SEARCH DEPTH FIRST BY "
10955        })?;
10956        render_ident_list(&self.columns, ctx, f)?;
10957        f.write_str(" SET ")?;
10958        self.set_column.render(ctx, f)
10959    }
10960}
10961
10962impl<X: Extension + Render> Render for CteCycleClause<X> {
10963    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10964        f.write_str("CYCLE ")?;
10965        render_ident_list(&self.columns, ctx, f)?;
10966        f.write_str(" SET ")?;
10967        self.mark_column.render(ctx, f)?;
10968        // The `TO value DEFAULT default` mark pair, when present, renders its own leading
10969        // space; the short form emits nothing between the mark column and `USING`.
10970        if let Some(mark) = &self.mark {
10971            mark.render(ctx, f)?;
10972        }
10973        f.write_str(" USING ")?;
10974        self.path_column.render(ctx, f)
10975    }
10976}
10977
10978impl<X: Extension + Render> Render for CteCycleMark<X> {
10979    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10980        f.write_str(" TO ")?;
10981        self.value.render(ctx, f)?;
10982        f.write_str(" DEFAULT ")?;
10983        self.default.render(ctx, f)
10984    }
10985}
10986
10987impl<X: Extension + Render> Render for CteBody<X> {
10988    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10989        // Each DML arm's node renders its own leading `WITH` and `RETURNING` tail,
10990        // so a nested `WITH u AS (…) INSERT …` body round-trips through the arm.
10991        match self {
10992            Self::Query { query, .. } => query.render(ctx, f),
10993            Self::Insert { insert, .. } => insert.render(ctx, f),
10994            Self::Update { update, .. } => update.render(ctx, f),
10995            Self::Delete { delete, .. } => delete.render(ctx, f),
10996            Self::Merge { merge, .. } => merge.render(ctx, f),
10997        }
10998    }
10999}
11000
11001impl<X: Extension + Render> Render for Values<X> {
11002    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11003        f.write_str("VALUES ")?;
11004        render_values_rows(&self.rows, self.explicit_row, ctx, f)
11005    }
11006}
11007
11008impl<X: Extension + Render> Render for ValuesItem<X> {
11009    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11010        match self {
11011            Self::Expr { expr, .. } => expr.render(ctx, f),
11012            Self::Default { default, .. } => default.render(ctx, f),
11013        }
11014    }
11015}
11016
11017/// Render the `SELECT [DISTINCT …] [STRAIGHT_JOIN] <projection>` head shared by the
11018/// SELECT-first and FROM-first spellings. Writes the leading `SELECT`; any preceding
11019/// space is the caller's.
11020fn render_select_projection_clause<X: Extension + Render>(
11021    select: &Select<X>,
11022    ctx: &RenderCtx<'_>,
11023    f: &mut fmt::Formatter<'_>,
11024) -> fmt::Result {
11025    f.write_str("SELECT")?;
11026    match &select.distinct {
11027        None => {}
11028        Some(SelectDistinct::Quantifier { quantifier, .. }) => {
11029            f.write_str(" ")?;
11030            quantifier.render(ctx, f)?;
11031        }
11032        Some(SelectDistinct::On { exprs, .. }) => {
11033            f.write_str(" DISTINCT ON (")?;
11034            render_comma_separated(exprs, ctx, f)?;
11035            f.write_str(")")?;
11036        }
11037    }
11038    // MySQL `SELECT STRAIGHT_JOIN ...`, after the quantifier and before the list.
11039    if select.straight_join {
11040        f.write_str(" STRAIGHT_JOIN")?;
11041    }
11042    render_leading_space_comma_separated(&select.projection, ctx, f)
11043}
11044
11045/// Render the `[WHERE …] [GROUP BY … | GROUP BY ALL] [HAVING …] [WINDOW …] [QUALIFY …]`
11046/// tail shared by both spellings — every clause after the projection/`FROM` prefix, in
11047/// SQL order.
11048fn render_select_body_tail<X: Extension + Render>(
11049    select: &Select<X>,
11050    ctx: &RenderCtx<'_>,
11051    f: &mut fmt::Formatter<'_>,
11052) -> fmt::Result {
11053    // Hive/Spark lateral views trail the FROM clause and precede WHERE (the parser
11054    // only fills the field after a non-empty FROM).
11055    for view in &select.lateral_views {
11056        f.write_str(" ")?;
11057        view.render(ctx, f)?;
11058    }
11059    if let Some(selection) = &select.selection {
11060        f.write_str(" WHERE ")?;
11061        selection.render(ctx, f)?;
11062    }
11063    // The Oracle-style hierarchical query clause sits after WHERE and before GROUP BY;
11064    // the node renders its `START WITH`/`CONNECT BY` pair in the written order.
11065    if let Some(connect_by) = &select.connect_by {
11066        f.write_str(" ")?;
11067        connect_by.render(ctx, f)?;
11068    }
11069    if !select.group_by.is_empty() {
11070        f.write_str(" GROUP BY ")?;
11071        // PostgreSQL's grouping-set quantifier prefixes the item list (`GROUP BY DISTINCT
11072        // <items>`); the parser only sets it alongside a non-empty list, so it renders here.
11073        if let Some(quantifier) = &select.group_by_quantifier {
11074            quantifier.render(ctx, f)?;
11075            f.write_str(" ")?;
11076        }
11077        render_comma_separated(&select.group_by, ctx, f)?;
11078    }
11079    // DuckDB's `GROUP BY ALL` mode; the parser never sets it alongside a
11080    // non-empty `group_by` (the engine rejects mixing), so at most one arm fires.
11081    // The `*` shorthand round-trips only under a source-fidelity render; a
11082    // target-dialect re-spell and the redacted fingerprint canonicalize to `ALL`.
11083    if let Some(spelling) = select.group_by_all {
11084        let bare_star = spelling == GroupByAllSpelling::Star && honours_source_spelling(ctx);
11085        f.write_str(if bare_star {
11086            " GROUP BY *"
11087        } else {
11088            " GROUP BY ALL"
11089        })?;
11090    }
11091    if let Some(having) = &select.having {
11092        f.write_str(" HAVING ")?;
11093        having.render(ctx, f)?;
11094    }
11095    if !select.windows.is_empty() {
11096        f.write_str(" WINDOW ")?;
11097        render_comma_separated(&select.windows, ctx, f)?;
11098    }
11099    // QUALIFY follows the WINDOW clause — DuckDB's grammar order (`QUALIFY …
11100    // WINDOW …` is a DuckDB syntax error), so emitting it last round-trips.
11101    if let Some(qualify) = &select.qualify {
11102        f.write_str(" QUALIFY ")?;
11103        qualify.render(ctx, f)?;
11104    }
11105    // DuckDB's `USING SAMPLE` sample clause follows QUALIFY and precedes the enclosing
11106    // query's `ORDER BY` (the reverse order is a DuckDB syntax error).
11107    if let Some(sample) = &select.sample {
11108        f.write_str(" USING SAMPLE ")?;
11109        sample.render(ctx, f)?;
11110    }
11111    Ok(())
11112}
11113
11114/// True when a FROM-first body carries the implicit `SELECT *` of the bare
11115/// `FROM <tables>` form — a single unmodified wildcard with no `DISTINCT`. That surface
11116/// round-trips to `FROM <tables>` with the `SELECT *` left implicit (the one canonical
11117/// render for a FROM-first wildcard projection; an explicit `FROM t SELECT *` normalizes
11118/// onto it).
11119fn from_first_projection_is_implicit<X: Extension>(select: &Select<X>) -> bool {
11120    select.distinct.is_none()
11121        && matches!(select.projection.as_slice(), [SelectItem::Wildcard { .. }])
11122}
11123
11124impl<X: Extension + Render> Render for Select<X> {
11125    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11126        match self.spelling {
11127            // `TABLE name` round-trips its short-form spelling rather than expanding to
11128            // the canonical `SELECT * FROM name`. The parser is the only constructor of
11129            // this tag and guarantees the shape — a wildcard projection over one relation
11130            // — so the relation alone renders the whole command (ADR-0011).
11131            SelectSpelling::TableCommand => {
11132                f.write_str("TABLE ")?;
11133                if let Some(table) = self.from.first() {
11134                    table.relation.render(ctx, f)?;
11135                }
11136                Ok(())
11137            }
11138            // DuckDB's FROM-first order: the `FROM` clause leads, then the projection
11139            // (dropped when it is the bare `FROM t` implicit `SELECT *`), then the shared
11140            // tail. Every clause after the projection sits in its ordinary place.
11141            SelectSpelling::FromFirst => {
11142                f.write_str("FROM ")?;
11143                render_comma_separated(&self.from, ctx, f)?;
11144                if !from_first_projection_is_implicit(self) {
11145                    f.write_str(" ")?;
11146                    render_select_projection_clause(self, ctx, f)?;
11147                }
11148                render_select_body_tail(self, ctx, f)
11149            }
11150            SelectSpelling::Select => {
11151                render_select_projection_clause(self, ctx, f)?;
11152                // PostgreSQL `SELECT … INTO <table>` sits between the projection and `FROM`.
11153                if let Some(into) = &self.into {
11154                    f.write_str(" ")?;
11155                    into.render(ctx, f)?;
11156                }
11157                if !self.from.is_empty() {
11158                    f.write_str(" FROM ")?;
11159                    render_comma_separated(&self.from, ctx, f)?;
11160                }
11161                render_select_body_tail(self, ctx, f)
11162            }
11163        }
11164    }
11165}
11166
11167impl<X: Extension + Render> Render for LateralView<X> {
11168    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11169        f.write_str("LATERAL VIEW ")?;
11170        if self.outer {
11171            f.write_str("OUTER ")?;
11172        }
11173        self.function.render(ctx, f)?;
11174        f.write_str(" ")?;
11175        self.alias.render(ctx, f)?;
11176        if !self.columns.is_empty() {
11177            // The `AS` is canonical: the AS-less Spark spelling re-renders with the
11178            // keyword (a structural, not byte-exact, round-trip — see the node doc).
11179            f.write_str(" AS ")?;
11180            render_comma_separated(&self.columns, ctx, f)?;
11181        }
11182        Ok(())
11183    }
11184}
11185
11186impl<X: Extension + Render> Render for HierarchicalClause<X> {
11187    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11188        // `START WITH` and `CONNECT BY` render in the written order (Oracle admits
11189        // either); `NOCYCLE` always modifies `CONNECT BY`, whichever side it lands on.
11190        let render_start_with = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
11191            if let Some(start_with) = &self.start_with {
11192                f.write_str("START WITH ")?;
11193                start_with.render(ctx, f)?;
11194            }
11195            Ok(())
11196        };
11197        let render_connect_by = |f: &mut fmt::Formatter<'_>| -> fmt::Result {
11198            f.write_str("CONNECT BY ")?;
11199            if self.nocycle {
11200                f.write_str("NOCYCLE ")?;
11201            }
11202            self.connect_by.render(ctx, f)
11203        };
11204        if self.start_with_leads && self.start_with.is_some() {
11205            render_start_with(f)?;
11206            f.write_str(" ")?;
11207            render_connect_by(f)
11208        } else if self.start_with.is_some() {
11209            render_connect_by(f)?;
11210            f.write_str(" ")?;
11211            render_start_with(f)
11212        } else {
11213            render_connect_by(f)
11214        }
11215    }
11216}
11217
11218impl Render for IntoTarget {
11219    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11220        f.write_str("INTO ")?;
11221        if let Some(temporary) = self.temporary {
11222            temporary.render(ctx, f)?;
11223            f.write_str(" ")?;
11224        }
11225        self.name.render(ctx, f)
11226    }
11227}
11228
11229impl<X: Extension + Render> Render for SelectItem<X> {
11230    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11231        match self {
11232            SelectItem::Wildcard {
11233                options,
11234                alias,
11235                alias_spelling,
11236                ..
11237            } => {
11238                f.write_str("*")?;
11239                render_optional_wildcard_options(options.as_deref(), ctx, f)?;
11240                render_alias(alias.as_ref(), *alias_spelling, ctx, f)
11241            }
11242            SelectItem::QualifiedWildcard {
11243                name,
11244                options,
11245                alias,
11246                alias_spelling,
11247                ..
11248            } => {
11249                name.render(ctx, f)?;
11250                f.write_str(".*")?;
11251                render_optional_wildcard_options(options.as_deref(), ctx, f)?;
11252                render_alias(alias.as_ref(), *alias_spelling, ctx, f)
11253            }
11254            SelectItem::Expr {
11255                expr,
11256                alias,
11257                alias_spelling,
11258                ..
11259            } => {
11260                // DuckDB's prefix form `alias: expr` writes the alias before the
11261                // value; a source-fidelity render reproduces it, a normalizing render
11262                // (`TargetDialect`/`Redacted`) falls through to the canonical trailing
11263                // `AS`.
11264                if let (Some(alias), AliasSpelling::PrefixColon) = (alias.as_ref(), alias_spelling)
11265                {
11266                    if honours_alias_spelling(ctx) {
11267                        alias.render(ctx, f)?;
11268                        f.write_str(": ")?;
11269                        return expr.render(ctx, f);
11270                    }
11271                }
11272                expr.render(ctx, f)?;
11273                if let Some(alias) = alias {
11274                    f.write_str(alias_lead(*alias_spelling, ctx))?;
11275                    alias.render(ctx, f)?;
11276                }
11277                Ok(())
11278            }
11279        }
11280    }
11281}
11282
11283/// Render the `EXCLUDE`/`REPLACE`/`RENAME` wildcard-modifier tail in DuckDB's fixed
11284/// canonical order, writing only the non-empty lists. Each list is parenthesized —
11285/// DuckDB's general spelling, which re-parses for the single-item bare form too (the
11286/// round-trip contract is structural, not byte-exact).
11287fn render_wildcard_options<X: Extension + Render>(
11288    options: &WildcardOptions<X>,
11289    ctx: &RenderCtx<'_>,
11290    f: &mut fmt::Formatter<'_>,
11291) -> fmt::Result {
11292    if !options.exclude.is_empty() {
11293        f.write_str(" EXCLUDE (")?;
11294        render_comma_separated(&options.exclude, ctx, f)?;
11295        f.write_str(")")?;
11296    }
11297    if !options.replace.is_empty() {
11298        f.write_str(" REPLACE (")?;
11299        render_comma_separated(&options.replace, ctx, f)?;
11300        f.write_str(")")?;
11301    }
11302    if !options.rename.is_empty() {
11303        f.write_str(" RENAME (")?;
11304        render_comma_separated(&options.rename, ctx, f)?;
11305        f.write_str(")")?;
11306    }
11307    Ok(())
11308}
11309
11310fn render_optional_wildcard_options<X: Extension + Render>(
11311    options: Option<&WildcardOptions<X>>,
11312    ctx: &RenderCtx<'_>,
11313    f: &mut fmt::Formatter<'_>,
11314) -> fmt::Result {
11315    match options {
11316        Some(options) => render_wildcard_options(options, ctx, f),
11317        None => Ok(()),
11318    }
11319}
11320
11321/// Render a list-comprehension source: a general expression, or the DuckDB column-star
11322/// `*` / `(* EXCLUDE (i))` form with its recorded parenthesization and wildcard modifiers.
11323fn render_comprehension_source<X: Extension + Render>(
11324    source: &ComprehensionSource<X>,
11325    ctx: &RenderCtx<'_>,
11326    f: &mut fmt::Formatter<'_>,
11327) -> fmt::Result {
11328    match source {
11329        ComprehensionSource::Expr { expr, .. } => expr.render(ctx, f),
11330        ComprehensionSource::Star {
11331            parenthesized,
11332            options,
11333            ..
11334        } => {
11335            if *parenthesized {
11336                f.write_str("(*")?;
11337                render_optional_wildcard_options(options.as_deref(), ctx, f)?;
11338                f.write_str(")")
11339            } else {
11340                f.write_str("*")?;
11341                render_optional_wildcard_options(options.as_deref(), ctx, f)
11342            }
11343        }
11344    }
11345}
11346
11347impl<X: Extension + Render> Render for WildcardReplace<X> {
11348    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11349        self.expr.render(ctx, f)?;
11350        f.write_str(" AS ")?;
11351        self.column.render(ctx, f)
11352    }
11353}
11354
11355impl Render for WildcardRename {
11356    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11357        self.column.render(ctx, f)?;
11358        f.write_str(" AS ")?;
11359        self.alias.render(ctx, f)
11360    }
11361}
11362
11363impl<X: Extension + Render> Render for GroupByItem<X> {
11364    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11365        match self {
11366            GroupByItem::Expr { expr, .. } => expr.render(ctx, f),
11367            GroupByItem::Rollup {
11368                exprs, spelling, ..
11369            } => match spelling {
11370                RollupSpelling::Function => {
11371                    f.write_str("ROLLUP (")?;
11372                    render_comma_separated(exprs, ctx, f)?;
11373                    f.write_str(")")
11374                }
11375                // MySQL's trailing modifier. The parser wraps the whole key list into
11376                // this single item, so it is the sole GROUP BY item — the GROUP BY
11377                // list's comma separator never fires between it and a sibling, so the
11378                // per-item renderer can emit the trailing form directly (no one-item
11379                // special case needed in the Select group_by rendering).
11380                RollupSpelling::WithRollup => {
11381                    render_comma_separated(exprs, ctx, f)?;
11382                    f.write_str(" WITH ROLLUP")
11383                }
11384            },
11385            GroupByItem::Cube { exprs, .. } => {
11386                f.write_str("CUBE (")?;
11387                render_comma_separated(exprs, ctx, f)?;
11388                f.write_str(")")
11389            }
11390            GroupByItem::GroupingSets { sets, .. } => {
11391                f.write_str("GROUPING SETS (")?;
11392                render_comma_separated(sets, ctx, f)?;
11393                f.write_str(")")
11394            }
11395            GroupByItem::Empty { .. } => f.write_str("()"),
11396        }
11397    }
11398}
11399
11400// ---------------------------------------------------------------------------
11401// FROM clause
11402// ---------------------------------------------------------------------------
11403
11404impl<X: Extension + Render> Render for TableWithJoins<X> {
11405    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11406        self.relation.render(ctx, f)?;
11407        for join in &self.joins {
11408            f.write_str(" ")?;
11409            join.render(ctx, f)?;
11410        }
11411        Ok(())
11412    }
11413}
11414
11415impl<X: Extension + Render> Render for TableFactor<X> {
11416    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11417        match self {
11418            TableFactor::Table {
11419                name,
11420                inheritance,
11421                json_path,
11422                version,
11423                partition,
11424                alias,
11425                indexed_by,
11426                index_hints,
11427                sample,
11428                table_hints,
11429                ..
11430            } => {
11431                render_relation_inheritance(inheritance, name, ctx, f)?;
11432                // A PartiQL / SUPER JSON path, attached directly to the table name with no
11433                // separating space (`FROM src[0].a`). The root is a bracket index; each
11434                // segment renders as its own suffix (`[index]` or `.key`).
11435                for segment in json_path {
11436                    render_semi_structured_path_segment(segment, true, ctx, f)?;
11437                }
11438                // A version / time-travel modifier, between the table name and the alias.
11439                if let Some(version) = version {
11440                    f.write_str(" ")?;
11441                    version.render(ctx, f)?;
11442                }
11443                // MySQL `PARTITION (p0, p1)`, between the table name and the alias.
11444                if !partition.is_empty() {
11445                    f.write_str(" PARTITION (")?;
11446                    render_comma_separated(partition, ctx, f)?;
11447                    f.write_str(")")?;
11448                }
11449                render_table_alias(alias.as_deref(), ctx, f)?;
11450                // SQLite `INDEXED BY <index>` / `NOT INDEXED`, immediately after the alias.
11451                if let Some(indexed_by) = indexed_by {
11452                    f.write_str(" ")?;
11453                    indexed_by.render(ctx, f)?;
11454                }
11455                // MySQL index hints, after the alias, space-separated (no comma).
11456                for hint in index_hints {
11457                    f.write_str(" ")?;
11458                    hint.render(ctx, f)?;
11459                }
11460                if let Some(sample) = sample {
11461                    sample.render(ctx, f)?;
11462                }
11463                // MSSQL `WITH (...)` table hints, after the tablesample clause,
11464                // comma-separated inside one parenthesized list.
11465                if !table_hints.is_empty() {
11466                    f.write_str(" WITH (")?;
11467                    render_comma_separated(table_hints, ctx, f)?;
11468                    f.write_str(")")?;
11469                }
11470                Ok(())
11471            }
11472            TableFactor::Derived {
11473                lateral,
11474                subquery,
11475                alias,
11476                spelling,
11477                ..
11478            } => {
11479                if *lateral {
11480                    f.write_str("LATERAL ")?;
11481                }
11482                match spelling {
11483                    DerivedSpelling::Parenthesized => {
11484                        f.write_str("(")?;
11485                        subquery.render(ctx, f)?;
11486                        f.write_str(")")?;
11487                    }
11488                    // DuckDB's bare `FROM VALUES (…) AS t`: the body (always a `VALUES`
11489                    // constructor) renders without the wrapping parentheses; the alias
11490                    // the parser required trails as usual via `render_table_alias`.
11491                    DerivedSpelling::BareValues => subquery.render(ctx, f)?,
11492                }
11493                render_table_alias(alias.as_deref(), ctx, f)
11494            }
11495            TableFactor::Function {
11496                lateral,
11497                function,
11498                with_ordinality,
11499                alias,
11500                column_defs,
11501                ..
11502            } => {
11503                if *lateral {
11504                    f.write_str("LATERAL ")?;
11505                }
11506                function.render(ctx, f)?;
11507                if *with_ordinality {
11508                    f.write_str(" WITH ORDINALITY")?;
11509                }
11510                render_function_alias(alias.as_deref(), column_defs, ctx, f)
11511            }
11512            TableFactor::RowsFrom {
11513                lateral,
11514                functions,
11515                with_ordinality,
11516                alias,
11517                ..
11518            } => {
11519                if *lateral {
11520                    f.write_str("LATERAL ")?;
11521                }
11522                f.write_str("ROWS FROM (")?;
11523                render_comma_separated(functions, ctx, f)?;
11524                f.write_str(")")?;
11525                if *with_ordinality {
11526                    f.write_str(" WITH ORDINALITY")?;
11527                }
11528                render_table_alias(alias.as_deref(), ctx, f)
11529            }
11530            TableFactor::Unnest {
11531                lateral,
11532                array_exprs,
11533                with_ordinality,
11534                alias,
11535                column_defs,
11536                with_offset,
11537                with_offset_alias,
11538                ..
11539            } => {
11540                if *lateral {
11541                    f.write_str("LATERAL ")?;
11542                }
11543                f.write_str("UNNEST(")?;
11544                render_comma_separated(array_exprs, ctx, f)?;
11545                f.write_str(")")?;
11546                // `WITH ORDINALITY` precedes the alias (PostgreSQL/DuckDB); `WITH OFFSET`
11547                // follows it (BigQuery). The two never co-occur, so rendering each at its
11548                // grammar position re-parses cleanly under whichever dialect produced it.
11549                if *with_ordinality {
11550                    f.write_str(" WITH ORDINALITY")?;
11551                }
11552                render_function_alias(alias.as_deref(), column_defs, ctx, f)?;
11553                if *with_offset {
11554                    f.write_str(" WITH OFFSET")?;
11555                    // The `WITH OFFSET` position carries no spelling tag; it keeps the
11556                    // canonical `AS`, unchanged by this fidelity pass.
11557                    render_alias(with_offset_alias.as_ref(), AliasSpelling::As, ctx, f)?;
11558                }
11559                Ok(())
11560            }
11561            TableFactor::NestedJoin { table, alias, .. } => {
11562                f.write_str("(")?;
11563                table.render(ctx, f)?;
11564                f.write_str(")")?;
11565                render_table_alias(alias.as_deref(), ctx, f)
11566            }
11567            TableFactor::SpecialFunction {
11568                keyword,
11569                precision,
11570                alias,
11571                ..
11572            } => {
11573                f.write_str(special_function_keyword(*keyword))?;
11574                if let Some(precision) = precision {
11575                    write!(f, "({precision})")?;
11576                }
11577                render_table_alias(alias.as_deref(), ctx, f)
11578            }
11579            // A statement-spelled core in factor position is the parenthesized
11580            // statement form (`FROM (PIVOT t ON …)`), so the parentheses rederive
11581            // from the spelling tag; the suffix spelling needs none of its own.
11582            TableFactor::Pivot { pivot, alias, .. } => {
11583                if matches!(pivot.spelling, PivotSpelling::Statement) {
11584                    f.write_str("(")?;
11585                    pivot.render(ctx, f)?;
11586                    f.write_str(")")?;
11587                } else {
11588                    pivot.render(ctx, f)?;
11589                }
11590                render_table_alias(alias.as_deref(), ctx, f)
11591            }
11592            TableFactor::Unpivot { unpivot, alias, .. } => {
11593                if matches!(unpivot.spelling, UnpivotSpelling::Statement) {
11594                    f.write_str("(")?;
11595                    unpivot.render(ctx, f)?;
11596                    f.write_str(")")?;
11597                } else {
11598                    unpivot.render(ctx, f)?;
11599                }
11600                render_table_alias(alias.as_deref(), ctx, f)
11601            }
11602            TableFactor::MatchRecognize {
11603                match_recognize,
11604                alias,
11605                ..
11606            } => {
11607                match_recognize.render(ctx, f)?;
11608                render_table_alias(alias.as_deref(), ctx, f)
11609            }
11610            // DuckDB's `SHOW_REF` table source is always written parenthesized in
11611            // `FROM` (`FROM (DESCRIBE …)`, `FROM (SHOW databases)`); the parentheses are
11612            // load-bearing (a bare leading keyword is a top-level statement), so they
11613            // render unconditionally, like the statement-spelled `TableFactor::Pivot`.
11614            TableFactor::ShowRef { show, alias, .. } => {
11615                f.write_str("(")?;
11616                show.render(ctx, f)?;
11617                f.write_str(")")?;
11618                render_table_alias(alias.as_deref(), ctx, f)
11619            }
11620            TableFactor::JsonTable {
11621                json_table, alias, ..
11622            } => {
11623                json_table.render(ctx, f)?;
11624                render_table_alias(alias.as_deref(), ctx, f)
11625            }
11626            TableFactor::XmlTable {
11627                xml_table, alias, ..
11628            } => {
11629                xml_table.render(ctx, f)?;
11630                render_table_alias(alias.as_deref(), ctx, f)
11631            }
11632            TableFactor::OpenJson {
11633                open_json, alias, ..
11634            } => {
11635                open_json.render(ctx, f)?;
11636                render_table_alias(alias.as_deref(), ctx, f)
11637            }
11638            TableFactor::TableExpr { expr, alias, .. } => {
11639                f.write_str("TABLE(")?;
11640                expr.render(ctx, f)?;
11641                f.write_str(")")?;
11642                render_table_alias(alias.as_deref(), ctx, f)
11643            }
11644            TableFactor::Other { ext, .. } => ext.render(ctx, f),
11645        }
11646    }
11647}
11648
11649impl<X: Extension + Render> Render for ShowRef<X> {
11650    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11651        f.write_str(match self.kind {
11652            ShowRefKind::Describe => "DESCRIBE",
11653            ShowRefKind::Desc => "DESC",
11654            ShowRefKind::Show => "SHOW",
11655            ShowRefKind::Summarize => "SUMMARIZE",
11656        })?;
11657        match &self.target {
11658            ShowRefTarget::Empty { .. } => Ok(()),
11659            ShowRefTarget::Query { query, .. } => {
11660                f.write_str(" ")?;
11661                query.render(ctx, f)
11662            }
11663            ShowRefTarget::Name { name, .. } => {
11664                f.write_str(" ")?;
11665                name.render(ctx, f)
11666            }
11667        }
11668    }
11669}
11670
11671impl<X: Extension + Render> Render for JsonTable<X> {
11672    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11673        if self.lateral {
11674            f.write_str("LATERAL ")?;
11675        }
11676        f.write_str("JSON_TABLE(")?;
11677        self.context.render(ctx, f)?;
11678        f.write_str(", ")?;
11679        self.path.render(ctx, f)?;
11680        if let Some(name) = &self.path_name {
11681            f.write_str(" AS ")?;
11682            name.render(ctx, f)?;
11683        }
11684        if !self.passing.is_empty() {
11685            f.write_str(" PASSING ")?;
11686            render_comma_separated(&self.passing, ctx, f)?;
11687        }
11688        f.write_str(" COLUMNS (")?;
11689        render_comma_separated(&self.columns, ctx, f)?;
11690        f.write_str(")")?;
11691        render_json_on_behavior(&self.on_error, "ERROR", ctx, f)?;
11692        f.write_str(")")
11693    }
11694}
11695
11696impl<X: Extension + Render> Render for JsonTableColumn<X> {
11697    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11698        match self {
11699            JsonTableColumn::ForOrdinality { name, .. } => {
11700                name.render(ctx, f)?;
11701                f.write_str(" FOR ORDINALITY")
11702            }
11703            JsonTableColumn::Regular {
11704                name,
11705                data_type,
11706                format,
11707                path,
11708                wrapper,
11709                quotes,
11710                on_empty,
11711                on_error,
11712                ..
11713            } => {
11714                name.render(ctx, f)?;
11715                f.write_str(" ")?;
11716                data_type.render(ctx, f)?;
11717                if let Some(format) = format {
11718                    f.write_str(" ")?;
11719                    format.render(ctx, f)?;
11720                }
11721                if let Some(path) = path {
11722                    f.write_str(" PATH ")?;
11723                    path.render(ctx, f)?;
11724                }
11725                render_json_wrapper(*wrapper, f)?;
11726                render_json_quotes(*quotes, f)?;
11727                render_json_on_behavior(on_empty, "EMPTY", ctx, f)?;
11728                render_json_on_behavior(on_error, "ERROR", ctx, f)
11729            }
11730            JsonTableColumn::Exists {
11731                name,
11732                data_type,
11733                path,
11734                on_error,
11735                ..
11736            } => {
11737                name.render(ctx, f)?;
11738                f.write_str(" ")?;
11739                data_type.render(ctx, f)?;
11740                f.write_str(" EXISTS")?;
11741                if let Some(path) = path {
11742                    f.write_str(" PATH ")?;
11743                    path.render(ctx, f)?;
11744                }
11745                render_json_on_behavior(on_error, "ERROR", ctx, f)
11746            }
11747            JsonTableColumn::Nested {
11748                path,
11749                path_name,
11750                columns,
11751                ..
11752            } => {
11753                f.write_str("NESTED PATH ")?;
11754                path.render(ctx, f)?;
11755                if let Some(name) = path_name {
11756                    f.write_str(" AS ")?;
11757                    name.render(ctx, f)?;
11758                }
11759                f.write_str(" COLUMNS (")?;
11760                render_comma_separated(columns, ctx, f)?;
11761                f.write_str(")")
11762            }
11763        }
11764    }
11765}
11766
11767/// Render an expression that occupies a PostgreSQL `c_expr` operand position, wrapping it in
11768/// parentheses when it is a compound `a_expr` (a binary/unary operator, predicate, …) rather
11769/// than a `c_expr` primary. In a `c_expr` slot a bare `a || b` is a syntax error, so a parsed
11770/// `(a || b)` must re-emit its parentheses to round-trip; a primary (a literal, column,
11771/// function call, parenthesized/atom form) needs none. Redundant parentheses on a primary are
11772/// harmless — PostgreSQL folds them away — so the allowlist errs toward the common atoms.
11773fn render_c_expr_operand<X: Extension + Render>(
11774    expr: &Expr<X>,
11775    ctx: &RenderCtx<'_>,
11776    f: &mut fmt::Formatter<'_>,
11777) -> fmt::Result {
11778    let is_primary = matches!(
11779        expr,
11780        Expr::Column { .. }
11781            | Expr::Literal { .. }
11782            | Expr::Function { .. }
11783            | Expr::Parameter { .. }
11784            | Expr::PositionalColumn { .. }
11785            | Expr::SessionVariable { .. }
11786            | Expr::SpecialFunction { .. }
11787    );
11788    if is_primary {
11789        expr.render(ctx, f)
11790    } else {
11791        f.write_str("(")?;
11792        expr.render(ctx, f)?;
11793        f.write_str(")")
11794    }
11795}
11796
11797impl<X: Extension + Render> Render for XmlTable<X> {
11798    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11799        if self.lateral {
11800            f.write_str("LATERAL ")?;
11801        }
11802        f.write_str("XMLTABLE(")?;
11803        if !self.namespaces.is_empty() {
11804            f.write_str("XMLNAMESPACES(")?;
11805            render_comma_separated(&self.namespaces, ctx, f)?;
11806            f.write_str("), ")?;
11807        }
11808        render_c_expr_operand(&self.row_expr, ctx, f)?;
11809        f.write_str(" PASSING")?;
11810        render_xml_passing_mechanism(&self.passing_mechanism_before, f)?;
11811        f.write_str(" ")?;
11812        render_c_expr_operand(&self.document, ctx, f)?;
11813        render_xml_passing_mechanism(&self.passing_mechanism_after, f)?;
11814        f.write_str(" COLUMNS ")?;
11815        render_comma_separated(&self.columns, ctx, f)?;
11816        f.write_str(")")
11817    }
11818}
11819
11820impl<X: Extension + Render> Render for XmlNamespace<X> {
11821    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11822        match &self.name {
11823            Some(name) => {
11824                self.uri.render(ctx, f)?;
11825                f.write_str(" AS ")?;
11826                name.render(ctx, f)
11827            }
11828            None => {
11829                f.write_str("DEFAULT ")?;
11830                self.uri.render(ctx, f)
11831            }
11832        }
11833    }
11834}
11835
11836impl<X: Extension + Render> Render for XmlTableColumn<X> {
11837    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11838        match self {
11839            XmlTableColumn::ForOrdinality { name, .. } => {
11840                name.render(ctx, f)?;
11841                f.write_str(" FOR ORDINALITY")
11842            }
11843            XmlTableColumn::Regular {
11844                name,
11845                data_type,
11846                path,
11847                default,
11848                not_null,
11849                ..
11850            } => {
11851                name.render(ctx, f)?;
11852                f.write_str(" ")?;
11853                data_type.render(ctx, f)?;
11854                // Canonical order: PATH, DEFAULT, then the nullability declaration.
11855                // PostgreSQL admits these order-free and normalizes them into fixed
11856                // fields, so re-emitting one order round-trips to the same node.
11857                if let Some(path) = path {
11858                    f.write_str(" PATH ")?;
11859                    path.render(ctx, f)?;
11860                }
11861                if let Some(default) = default {
11862                    f.write_str(" DEFAULT ")?;
11863                    default.render(ctx, f)?;
11864                }
11865                match not_null {
11866                    Some(true) => f.write_str(" NOT NULL"),
11867                    Some(false) => f.write_str(" NULL"),
11868                    None => Ok(()),
11869                }
11870            }
11871        }
11872    }
11873}
11874
11875impl<X: Extension + Render> Render for OpenJson<X> {
11876    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11877        f.write_str("OPENJSON(")?;
11878        self.json_expr.render(ctx, f)?;
11879        if let Some(path) = &self.path {
11880            f.write_str(", ")?;
11881            path.render(ctx, f)?;
11882        }
11883        f.write_str(")")?;
11884        // An empty `columns` is the absent `WITH` clause (the default schema); a present
11885        // clause is always non-empty.
11886        if !self.columns.is_empty() {
11887            f.write_str(" WITH (")?;
11888            render_comma_separated(&self.columns, ctx, f)?;
11889            f.write_str(")")?;
11890        }
11891        Ok(())
11892    }
11893}
11894
11895impl<X: Extension + Render> Render for OpenJsonColumn<X> {
11896    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11897        self.name.render(ctx, f)?;
11898        f.write_str(" ")?;
11899        self.data_type.render(ctx, f)?;
11900        if let Some(path) = &self.path {
11901            f.write_str(" ")?;
11902            path.render(ctx, f)?;
11903        }
11904        if self.as_json {
11905            f.write_str(" AS JSON")?;
11906        }
11907        Ok(())
11908    }
11909}
11910
11911impl<X: Extension + Render> Render for Pivot<X> {
11912    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11913        match self.spelling {
11914            // `[WITH …] PIVOT <source> [ON …] [USING …] [GROUP BY …] [ORDER BY …]
11915            // [LIMIT …]` — the leading-keyword statement; every clause but the
11916            // source may be absent.
11917            PivotSpelling::Statement => {
11918                if let Some(with) = &self.with {
11919                    with.render(ctx, f)?;
11920                    f.write_str(" ")?;
11921                }
11922                f.write_str("PIVOT ")?;
11923                self.source.render(ctx, f)?;
11924                if !self.pivot_on.is_empty() {
11925                    f.write_str(" ON ")?;
11926                    render_comma_separated(&self.pivot_on, ctx, f)?;
11927                }
11928                if !self.aggregates.is_empty() {
11929                    f.write_str(" USING ")?;
11930                    render_comma_separated(&self.aggregates, ctx, f)?;
11931                }
11932                if !self.group_by.is_empty() {
11933                    f.write_str(" GROUP BY ")?;
11934                    render_comma_separated(&self.group_by, ctx, f)?;
11935                }
11936                render_pivot_statement_tail(
11937                    &self.order_by,
11938                    self.order_by_all.as_deref(),
11939                    self.limit.as_deref(),
11940                    ctx,
11941                    f,
11942                )
11943            }
11944            // `<source> PIVOT (<aggregates> FOR <col> IN (<values>) [GROUP BY …])` — the
11945            // table factor; exactly one `FOR` column (the enclosing alias is rendered by
11946            // the `TableFactor::Pivot` arm).
11947            PivotSpelling::TableFactor => {
11948                self.source.render(ctx, f)?;
11949                f.write_str(" PIVOT (")?;
11950                render_comma_separated(&self.aggregates, ctx, f)?;
11951                // One `FOR` keyword heads the whole column list; the extra heads
11952                // are written bare, space-separated (`FOR y IN (…) m IN (…)`).
11953                for (index, column) in self.pivot_on.iter().enumerate() {
11954                    f.write_str(if index == 0 { " FOR " } else { " " })?;
11955                    column.render(ctx, f)?;
11956                }
11957                if !self.group_by.is_empty() {
11958                    f.write_str(" GROUP BY ")?;
11959                    render_comma_separated(&self.group_by, ctx, f)?;
11960                }
11961                if let Some(default) = &self.default_on_null {
11962                    f.write_str(" DEFAULT ON NULL (")?;
11963                    default.render(ctx, f)?;
11964                    f.write_str(")")?;
11965                }
11966                f.write_str(")")
11967            }
11968        }
11969    }
11970}
11971
11972impl<X: Extension + Render> Render for Unpivot<X> {
11973    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11974        match self.spelling {
11975            // `[WITH …] UNPIVOT <source> ON <cols> [INTO NAME <name> VALUE <value>]
11976            // [ORDER BY …] [LIMIT …]`.
11977            UnpivotSpelling::Statement => {
11978                if let Some(with) = &self.with {
11979                    with.render(ctx, f)?;
11980                    f.write_str(" ")?;
11981                }
11982                f.write_str("UNPIVOT ")?;
11983                self.source.render(ctx, f)?;
11984                f.write_str(" ON ")?;
11985                render_comma_separated(&self.columns, ctx, f)?;
11986                if !self.name.is_empty() || !self.value.is_empty() {
11987                    f.write_str(" INTO NAME ")?;
11988                    render_comma_separated(&self.name, ctx, f)?;
11989                    f.write_str(" VALUE ")?;
11990                    render_comma_separated(&self.value, ctx, f)?;
11991                }
11992                render_pivot_statement_tail(
11993                    &self.order_by,
11994                    self.order_by_all.as_deref(),
11995                    self.limit.as_deref(),
11996                    ctx,
11997                    f,
11998                )
11999            }
12000            // `<source> UNPIVOT [INCLUDE|EXCLUDE NULLS] (<value> FOR <name> IN (<cols>))`.
12001            UnpivotSpelling::TableFactor => {
12002                self.source.render(ctx, f)?;
12003                f.write_str(" UNPIVOT ")?;
12004                // A written marker round-trips; the unwritten default (`None`) renders
12005                // bare (`EXCLUDE NULLS` semantics).
12006                match self.null_inclusion {
12007                    Some(NullInclusion::IncludeNulls) => f.write_str("INCLUDE NULLS ")?,
12008                    Some(NullInclusion::ExcludeNulls) => f.write_str("EXCLUDE NULLS ")?,
12009                    None => {}
12010                }
12011                f.write_str("(")?;
12012                render_unpivot_name_list(&self.value, ctx, f)?;
12013                f.write_str(" FOR ")?;
12014                render_unpivot_name_list(&self.name, ctx, f)?;
12015                f.write_str(" IN (")?;
12016                render_comma_separated(&self.columns, ctx, f)?;
12017                f.write_str("))")
12018            }
12019        }
12020    }
12021}
12022
12023/// Render a row-pattern list joined by `separator` (`" "` for concatenation, `" | "`
12024/// for alternation, `", "` for `PERMUTE`).
12025fn render_row_patterns(
12026    patterns: &[MatchRecognizePattern],
12027    separator: &str,
12028    ctx: &RenderCtx<'_>,
12029    f: &mut fmt::Formatter<'_>,
12030) -> fmt::Result {
12031    for (index, pattern) in patterns.iter().enumerate() {
12032        if index > 0 {
12033            f.write_str(separator)?;
12034        }
12035        pattern.render(ctx, f)?;
12036    }
12037    Ok(())
12038}
12039
12040impl<X: Extension + Render> Render for MatchRecognize<X> {
12041    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12042        // `<source> MATCH_RECOGNIZE ( [PARTITION BY …] [ORDER BY …] [MEASURES …]
12043        // [ROWS PER MATCH] [AFTER MATCH SKIP …] PATTERN (…) [SUBSET …] [DEFINE …] )`.
12044        // Every clause but `PATTERN` is optional; `wrote` tracks whether a preceding
12045        // clause has been emitted so the single-space separators stay tidy.
12046        self.source.render(ctx, f)?;
12047        f.write_str(" MATCH_RECOGNIZE (")?;
12048        let mut wrote = false;
12049        if !self.partition_by.is_empty() {
12050            f.write_str("PARTITION BY ")?;
12051            render_comma_separated(&self.partition_by, ctx, f)?;
12052            wrote = true;
12053        }
12054        if !self.order_by.is_empty() {
12055            if wrote {
12056                f.write_str(" ")?;
12057            }
12058            f.write_str("ORDER BY ")?;
12059            render_comma_separated(&self.order_by, ctx, f)?;
12060            wrote = true;
12061        }
12062        if !self.measures.is_empty() {
12063            if wrote {
12064                f.write_str(" ")?;
12065            }
12066            f.write_str("MEASURES ")?;
12067            render_comma_separated(&self.measures, ctx, f)?;
12068            wrote = true;
12069        }
12070        if let Some(rows_per_match) = &self.rows_per_match {
12071            if wrote {
12072                f.write_str(" ")?;
12073            }
12074            rows_per_match.render(ctx, f)?;
12075            wrote = true;
12076        }
12077        if let Some(after_match_skip) = &self.after_match_skip {
12078            if wrote {
12079                f.write_str(" ")?;
12080            }
12081            after_match_skip.render(ctx, f)?;
12082            wrote = true;
12083        }
12084        if wrote {
12085            f.write_str(" ")?;
12086        }
12087        f.write_str("PATTERN (")?;
12088        self.pattern.render(ctx, f)?;
12089        f.write_str(")")?;
12090        if !self.subsets.is_empty() {
12091            f.write_str(" SUBSET ")?;
12092            render_comma_separated(&self.subsets, ctx, f)?;
12093        }
12094        if !self.define.is_empty() {
12095            f.write_str(" DEFINE ")?;
12096            render_comma_separated(&self.define, ctx, f)?;
12097        }
12098        f.write_str(")")
12099    }
12100}
12101
12102impl<X: Extension + Render> Render for Measure<X> {
12103    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12104        self.expr.render(ctx, f)?;
12105        f.write_str(" AS ")?;
12106        self.alias.render(ctx, f)
12107    }
12108}
12109
12110impl Render for RowsPerMatch {
12111    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12112        match self {
12113            RowsPerMatch::OneRow => f.write_str("ONE ROW PER MATCH"),
12114            RowsPerMatch::AllRows(mode) => {
12115                f.write_str("ALL ROWS PER MATCH")?;
12116                if let Some(mode) = mode {
12117                    f.write_str(" ")?;
12118                    mode.render(ctx, f)?;
12119                }
12120                Ok(())
12121            }
12122        }
12123    }
12124}
12125
12126impl Render for EmptyMatchesMode {
12127    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12128        f.write_str(match self {
12129            EmptyMatchesMode::Show => "SHOW EMPTY MATCHES",
12130            EmptyMatchesMode::Omit => "OMIT EMPTY MATCHES",
12131            EmptyMatchesMode::WithUnmatched => "WITH UNMATCHED ROWS",
12132        })
12133    }
12134}
12135
12136impl Render for AfterMatchSkip {
12137    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12138        f.write_str("AFTER MATCH SKIP ")?;
12139        match self {
12140            AfterMatchSkip::PastLastRow { .. } => f.write_str("PAST LAST ROW"),
12141            AfterMatchSkip::ToNextRow { .. } => f.write_str("TO NEXT ROW"),
12142            AfterMatchSkip::ToFirst { symbol, .. } => {
12143                f.write_str("TO FIRST ")?;
12144                symbol.render(ctx, f)
12145            }
12146            AfterMatchSkip::ToLast { symbol, .. } => {
12147                f.write_str("TO LAST ")?;
12148                symbol.render(ctx, f)
12149            }
12150        }
12151    }
12152}
12153
12154impl Render for SubsetDefinition {
12155    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12156        self.name.render(ctx, f)?;
12157        f.write_str(" = (")?;
12158        render_comma_separated(&self.members, ctx, f)?;
12159        f.write_str(")")
12160    }
12161}
12162
12163impl<X: Extension + Render> Render for SymbolDefinition<X> {
12164    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12165        self.symbol.render(ctx, f)?;
12166        f.write_str(" AS ")?;
12167        self.definition.render(ctx, f)
12168    }
12169}
12170
12171impl Render for MatchRecognizePattern {
12172    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12173        match self {
12174            MatchRecognizePattern::Symbol { symbol, .. } => symbol.render(ctx, f),
12175            MatchRecognizePattern::Start { .. } => f.write_str("^"),
12176            MatchRecognizePattern::End { .. } => f.write_str("$"),
12177            MatchRecognizePattern::Concat { patterns, .. } => {
12178                render_row_patterns(patterns, " ", ctx, f)
12179            }
12180            MatchRecognizePattern::Alternation { patterns, .. } => {
12181                render_row_patterns(patterns, " | ", ctx, f)
12182            }
12183            MatchRecognizePattern::Group { pattern, .. } => {
12184                f.write_str("(")?;
12185                pattern.render(ctx, f)?;
12186                f.write_str(")")
12187            }
12188            MatchRecognizePattern::Exclude { pattern, .. } => {
12189                f.write_str("{- ")?;
12190                pattern.render(ctx, f)?;
12191                f.write_str(" -}")
12192            }
12193            MatchRecognizePattern::Permute { patterns, .. } => {
12194                f.write_str("PERMUTE(")?;
12195                render_row_patterns(patterns, ", ", ctx, f)?;
12196                f.write_str(")")
12197            }
12198            MatchRecognizePattern::Repetition {
12199                pattern,
12200                quantifier,
12201                ..
12202            } => {
12203                pattern.render(ctx, f)?;
12204                quantifier.render(ctx, f)
12205            }
12206        }
12207    }
12208}
12209
12210impl Render for RepetitionQuantifier {
12211    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12212        match self {
12213            RepetitionQuantifier::ZeroOrMore => f.write_str("*"),
12214            RepetitionQuantifier::OneOrMore => f.write_str("+"),
12215            RepetitionQuantifier::AtMostOne => f.write_str("?"),
12216            RepetitionQuantifier::Exactly(n) => write!(f, "{{{n}}}"),
12217            RepetitionQuantifier::AtLeast(n) => write!(f, "{{{n},}}"),
12218            RepetitionQuantifier::AtMost(m) => write!(f, "{{,{m}}}"),
12219            RepetitionQuantifier::Range(n, m) => write!(f, "{{{n},{m}}}"),
12220        }
12221    }
12222}
12223
12224impl<X: Extension + Render> Render for PivotExpr<X> {
12225    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12226        self.expr.render(ctx, f)?;
12227        render_alias(self.alias.as_ref(), self.alias_spelling, ctx, f)
12228    }
12229}
12230
12231impl<X: Extension + Render> Render for PivotValueSource<X> {
12232    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12233        match self {
12234            PivotValueSource::Any { order_by, .. } => {
12235                f.write_str("ANY")?;
12236                if !order_by.is_empty() {
12237                    f.write_str(" ORDER BY ")?;
12238                    render_comma_separated(order_by, ctx, f)?;
12239                }
12240                Ok(())
12241            }
12242            PivotValueSource::Subquery { query, .. } => query.render(ctx, f),
12243        }
12244    }
12245}
12246
12247impl<X: Extension + Render> Render for PivotColumn<X> {
12248    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12249        if let Some(enum_source) = &self.enum_source {
12250            self.expr.render(ctx, f)?;
12251            f.write_str(" IN ")?;
12252            return enum_source.render(ctx, f);
12253        }
12254        // The standard `IN (ANY [ORDER BY …])` / `IN (<subquery>)` sources — the left
12255        // operand rederives its grouping parens exactly like the value-list branch.
12256        if let Some(value_source) = &self.value_source {
12257            render_predicate_operand(
12258                &self.expr,
12259                ctx.target().binding_powers.range_predicate(),
12260                Side::Left,
12261                ctx,
12262                f,
12263            )?;
12264            f.write_str(" IN (")?;
12265            value_source.render(ctx, f)?;
12266            return f.write_str(")");
12267        }
12268        if self.values.is_empty() {
12269            return self.expr.render(ctx, f);
12270        }
12271        // The written `IN` binds at the range-predicate rank exactly like the `IN` predicate
12272        // the statement parse literally reads it as before unfolding, so the column is rendered
12273        // as that predicate's left operand — rederiving the grouping parens a comparison-or-looser
12274        // expression needs (ADR-0008; `(a = b) IN (false, true)` must not re-render as the invalid
12275        // `a = b IN (…)` chain).
12276        render_predicate_operand(
12277            &self.expr,
12278            ctx.target().binding_powers.range_predicate(),
12279            Side::Left,
12280            ctx,
12281            f,
12282        )?;
12283        f.write_str(" IN (")?;
12284        render_comma_separated(&self.values, ctx, f)?;
12285        f.write_str(")")
12286    }
12287}
12288
12289impl<X: Extension + Render> Render for UnpivotColumn<X> {
12290    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12291        // A single column renders bare (`a`); a group parenthesizes (`(a, b)`).
12292        if self.columns.len() == 1 {
12293            self.columns[0].render(ctx, f)?;
12294        } else {
12295            f.write_str("(")?;
12296            render_comma_separated(&self.columns, ctx, f)?;
12297            f.write_str(")")?;
12298        }
12299        render_alias(self.alias.as_ref(), self.alias_spelling, ctx, f)
12300    }
12301}
12302
12303/// Render an `UNPIVOT` value/name list: a single name bare (`v`), several parenthesized
12304/// (`(v1, v2)`), matching DuckDB's multi-column unpivot surface.
12305fn render_unpivot_name_list(
12306    names: &[Ident],
12307    ctx: &RenderCtx<'_>,
12308    f: &mut fmt::Formatter<'_>,
12309) -> fmt::Result {
12310    if names.len() == 1 {
12311        names[0].render(ctx, f)
12312    } else {
12313        f.write_str("(")?;
12314        render_comma_separated(names, ctx, f)?;
12315        f.write_str(")")
12316    }
12317}
12318
12319/// Render the pivot statements' trailing `ORDER BY` / `LIMIT` modifiers (the [`Query`]
12320/// tail pattern, minus the clauses the pivot statements have no grammar for). The
12321/// parser never populates `order_by_all` alongside `order_by` (the engine rejects
12322/// mixing), so the two arms cannot both fire.
12323fn render_pivot_statement_tail<X: Extension + Render>(
12324    order_by: &[OrderByExpr<X>],
12325    order_by_all: Option<&OrderByAll>,
12326    limit: Option<&Limit<X>>,
12327    ctx: &RenderCtx<'_>,
12328    f: &mut fmt::Formatter<'_>,
12329) -> fmt::Result {
12330    if !order_by.is_empty() {
12331        f.write_str(" ORDER BY ")?;
12332        render_comma_separated(order_by, ctx, f)?;
12333    }
12334    if let Some(all) = order_by_all {
12335        f.write_str(" ORDER BY ")?;
12336        all.render(ctx, f)?;
12337    }
12338    if let Some(limit) = limit {
12339        f.write_str(" ")?;
12340        limit.render(ctx, f)?;
12341    }
12342    Ok(())
12343}
12344
12345impl<X: Extension + Render> Render for RowsFromItem<X> {
12346    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12347        self.function.render(ctx, f)?;
12348        if !self.column_defs.is_empty() {
12349            f.write_str(" AS (")?;
12350            render_comma_separated(&self.column_defs, ctx, f)?;
12351            f.write_str(")")?;
12352        }
12353        Ok(())
12354    }
12355}
12356
12357impl<X: Extension + Render> Render for TableFunctionColumn<X> {
12358    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12359        self.name.render(ctx, f)?;
12360        f.write_str(" ")?;
12361        self.data_type.render(ctx, f)
12362    }
12363}
12364
12365impl Render for TableAlias {
12366    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12367        self.name.render(ctx, f)?;
12368        if !self.columns.is_empty() {
12369            f.write_str("(")?;
12370            render_ident_list(&self.columns, ctx, f)?;
12371            f.write_str(")")?;
12372        }
12373        Ok(())
12374    }
12375}
12376
12377impl<X: Extension + Render> Render for TableSample<X> {
12378    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12379        f.write_str(" TABLESAMPLE ")?;
12380        self.method.render(ctx, f)?;
12381        f.write_str(" (")?;
12382        render_comma_separated(&self.args, ctx, f)?;
12383        f.write_str(")")?;
12384        if let Some(repeatable) = &self.repeatable {
12385            f.write_str(" REPEATABLE (")?;
12386            repeatable.render(ctx, f)?;
12387            f.write_str(")")?;
12388        }
12389        Ok(())
12390    }
12391}
12392
12393impl Render for SampleClause {
12394    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12395        // Canonical (method-first) spelling of DuckDB's two equivalent entry surfaces
12396        // (ADR-0011): a named method wraps the count `method(size unit)`, then the seed
12397        // rides a trailing `REPEATABLE (seed)`; a bare count renders `size unit`. The
12398        // caller has already written the `USING SAMPLE ` lead.
12399        if let Some(method) = &self.method {
12400            method.render(ctx, f)?;
12401            f.write_str("(")?;
12402            self.size.render(ctx, f)?;
12403            self.unit.render(ctx, f)?;
12404            f.write_str(")")?;
12405            if let Some(seed) = &self.seed {
12406                f.write_str(" REPEATABLE (")?;
12407                seed.render(ctx, f)?;
12408                f.write_str(")")?;
12409            }
12410        } else {
12411            self.size.render(ctx, f)?;
12412            self.unit.render(ctx, f)?;
12413        }
12414        Ok(())
12415    }
12416}
12417
12418impl Render for SampleUnit {
12419    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12420        f.write_str(match self {
12421            SampleUnit::Count => "",
12422            SampleUnit::Rows => " ROWS",
12423            SampleUnit::Percent => " PERCENT",
12424            SampleUnit::PercentSign => "%",
12425        })
12426    }
12427}
12428
12429/// Whether this render honours the source alias-introducer tag ([`AliasSpelling`]),
12430/// versus normalizing every alias to the canonical `AS`.
12431///
12432/// Only a source-fidelity [`RenderSpelling::PreserveSource`] render honours it. A
12433/// `TargetDialect` re-spell canonicalizes to `AS` (the prefix-colon form has no
12434/// target-neutral spelling); the [`Redacted`](RenderMode::Redacted) fingerprint also
12435/// canonicalizes, so two statements differing only in `AS`-vs-bare share one
12436/// fingerprint — an alias introducer is cosmetic, like the keyword casing the mask
12437/// already erases.
12438fn honours_alias_spelling(ctx: &RenderCtx<'_>) -> bool {
12439    honours_source_spelling(ctx)
12440}
12441
12442/// Whether a source-spelling surface tag is replayed (a fidelity render) versus
12443/// normalized to its canonical spelling.
12444///
12445/// Only a source-fidelity [`RenderSpelling::PreserveSource`] render outside the
12446/// [`Redacted`](RenderMode::Redacted) fingerprint honours such a tag. A `TargetDialect`
12447/// re-spell emits the canonical form, and the redacted fingerprint canonicalizes too —
12448/// so two statements differing only in a cosmetic spelling (`<>` vs `!=`, `LEFT JOIN`
12449/// vs `LEFT OUTER JOIN`, `SET x = 1` vs `SET x TO 1`) share one fingerprint. The
12450/// operator/keyword spelling is cosmetic, like the keyword casing the mask already
12451/// erases. Shared by every keyword/operator spelling tag in this doctrine.
12452fn honours_source_spelling(ctx: &RenderCtx<'_>) -> bool {
12453    ctx.spelling() == RenderSpelling::PreserveSource && ctx.mode() != RenderMode::Redacted
12454}
12455
12456/// The lead written before an alias name: ` AS ` for an `AS`-introduced or
12457/// synthesized alias, a bare ` ` when the source omitted `AS` and the render honours
12458/// that ([`honours_alias_spelling`]). Mirrors the operator spelling tags: a fidelity
12459/// distinction, not a semantic one.
12460fn alias_lead(spelling: AliasSpelling, ctx: &RenderCtx<'_>) -> &'static str {
12461    if !honours_alias_spelling(ctx) {
12462        return " AS ";
12463    }
12464    match spelling {
12465        AliasSpelling::Bare => " ",
12466        AliasSpelling::As | AliasSpelling::PrefixColon => " AS ",
12467    }
12468}
12469
12470fn render_table_alias(
12471    alias: Option<&TableAlias>,
12472    ctx: &RenderCtx<'_>,
12473    f: &mut fmt::Formatter<'_>,
12474) -> fmt::Result {
12475    if let Some(alias) = alias {
12476        f.write_str(alias_lead(alias.spelling, ctx))?;
12477        alias.render(ctx, f)?;
12478    }
12479    Ok(())
12480}
12481
12482/// Render a table function's alias plus its column definition list. An empty
12483/// `column_defs` is a plain alias; otherwise the typed list renders as
12484/// `AS [name](col type, ...)` — PostgreSQL's record-returning form, where the
12485/// optional correlation name precedes the parenthesized definitions.
12486fn render_function_alias<X: Extension + Render>(
12487    alias: Option<&TableAlias>,
12488    column_defs: &[TableFunctionColumn<X>],
12489    ctx: &RenderCtx<'_>,
12490    f: &mut fmt::Formatter<'_>,
12491) -> fmt::Result {
12492    if column_defs.is_empty() {
12493        return render_table_alias(alias, ctx, f);
12494    }
12495    // The record-returning form `func(...) AS [name](col type, ...)`: the `AS` leads
12496    // the definition list even when the correlation name is elided, so a bare source
12497    // alias only drops the keyword, never the required column list.
12498    f.write_str(alias.map_or(" AS ", |a| alias_lead(a.spelling, ctx)))?;
12499    if let Some(alias) = alias {
12500        alias.name.render(ctx, f)?;
12501    }
12502    f.write_str("(")?;
12503    render_comma_separated(column_defs, ctx, f)?;
12504    f.write_str(")")
12505}
12506
12507fn render_alias(
12508    alias: Option<&Ident>,
12509    spelling: AliasSpelling,
12510    ctx: &RenderCtx<'_>,
12511    f: &mut fmt::Formatter<'_>,
12512) -> fmt::Result {
12513    if let Some(alias) = alias {
12514        f.write_str(alias_lead(spelling, ctx))?;
12515        alias.render(ctx, f)?;
12516    }
12517    Ok(())
12518}
12519
12520/// Render a PostgreSQL `relation_expr` reference: the relation `name` wrapped by
12521/// its inheritance marker. Shared by `TableFactor::Table` and `DmlTarget` so the
12522/// four spellings round-trip identically wherever a relation can appear.
12523fn render_relation_inheritance(
12524    inheritance: &RelationInheritance,
12525    name: &ObjectName,
12526    ctx: &RenderCtx<'_>,
12527    f: &mut fmt::Formatter<'_>,
12528) -> fmt::Result {
12529    match inheritance {
12530        RelationInheritance::Plain => name.render(ctx, f),
12531        RelationInheritance::Descendants => {
12532            name.render(ctx, f)?;
12533            f.write_str(" *")
12534        }
12535        RelationInheritance::Only(OnlySyntax::Bare) => {
12536            f.write_str("ONLY ")?;
12537            name.render(ctx, f)
12538        }
12539        RelationInheritance::Only(OnlySyntax::Parenthesized) => {
12540            f.write_str("ONLY (")?;
12541            name.render(ctx, f)?;
12542            f.write_str(")")
12543        }
12544    }
12545}
12546
12547fn render_ident_list(
12548    items: &[Ident],
12549    ctx: &RenderCtx<'_>,
12550    f: &mut fmt::Formatter<'_>,
12551) -> fmt::Result {
12552    render_comma_separated(items, ctx, f)
12553}
12554
12555/// Render a slice of renderable nodes joined by `, `.
12556fn render_comma_separated<T: Render>(
12557    items: &[T],
12558    ctx: &RenderCtx<'_>,
12559    f: &mut fmt::Formatter<'_>,
12560) -> fmt::Result {
12561    for (i, item) in items.iter().enumerate() {
12562        if i > 0 {
12563            f.write_str(", ")?;
12564        }
12565        item.render(ctx, f)?;
12566    }
12567    Ok(())
12568}
12569
12570/// Render a comma-separated list attached to a preceding keyword by a single
12571/// leading space (` a, b, c`), or nothing when empty — the shared shape of a
12572/// `SELECT` projection, a `WITH` CTE list, and a transaction-mode list.
12573fn render_leading_space_comma_separated<T: Render>(
12574    items: &[T],
12575    ctx: &RenderCtx<'_>,
12576    f: &mut fmt::Formatter<'_>,
12577) -> fmt::Result {
12578    if items.is_empty() {
12579        return Ok(());
12580    }
12581    f.write_str(" ")?;
12582    render_comma_separated(items, ctx, f)
12583}
12584
12585/// Render the parenthesized row tuples of a `VALUES` clause — `(a, b), (c, d)` —
12586/// shared by `InsertValues` and `Values`. The leading `VALUES ` keyword is the
12587/// caller's; this renders only the comma-separated rows, each a parenthesized
12588/// comma-separated item list.
12589fn render_values_rows<T: Render>(
12590    rows: &[ThinVec<T>],
12591    explicit_row: bool,
12592    ctx: &RenderCtx<'_>,
12593    f: &mut fmt::Formatter<'_>,
12594) -> fmt::Result {
12595    // MySQL spells the query-position constructor `ROW( ... )`; the bare `( ... )` is the
12596    // PostgreSQL/DuckDB/SQLite/ANSI spelling. The flag round-trips the `ROW` keyword.
12597    let open = if explicit_row { "ROW(" } else { "(" };
12598    for (i, row) in rows.iter().enumerate() {
12599        if i > 0 {
12600            f.write_str(", ")?;
12601        }
12602        f.write_str(open)?;
12603        render_comma_separated(row, ctx, f)?;
12604        f.write_str(")")?;
12605    }
12606    Ok(())
12607}
12608
12609impl<X: Extension + Render> Render for Join<X> {
12610    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12611        // Order is `<keyword> <relation> <constraint>`, so the relation sits
12612        // between the operator's keyword phrase and its trailing ON/USING clause.
12613        self.operator.render(ctx, f)?;
12614        f.write_str(" ")?;
12615        self.relation.render(ctx, f)?;
12616        if let Some(constraint) = self.operator.constraint() {
12617            constraint.render(ctx, f)?;
12618        }
12619        Ok(())
12620    }
12621}
12622
12623impl<X: Extension> JoinOperator<X> {
12624    /// The constraint embedded in a constraint-carrying join operator.
12625    fn constraint(&self) -> Option<&JoinConstraint<X>> {
12626        match self {
12627            JoinOperator::Inner { constraint, .. }
12628            | JoinOperator::LeftOuter { constraint, .. }
12629            | JoinOperator::RightOuter { constraint, .. }
12630            | JoinOperator::FullOuter { constraint, .. }
12631            | JoinOperator::AsOf { constraint, .. }
12632            | JoinOperator::Semi { constraint, .. }
12633            | JoinOperator::Anti { constraint, .. } => Some(constraint),
12634            JoinOperator::Cross { .. }
12635            | JoinOperator::Positional { .. }
12636            | JoinOperator::Apply { .. } => None,
12637        }
12638    }
12639}
12640
12641impl<X: Extension + Render> Render for JoinOperator<X> {
12642    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12643        // `NATURAL` is a prefix modifier on the keyword, not a trailing clause.
12644        if matches!(self.constraint(), Some(JoinConstraint::Natural { .. })) {
12645            f.write_str("NATURAL ")?;
12646        }
12647        // The optional `INNER`/`OUTER` keyword is exact-synonym noise: only a
12648        // source-fidelity render replays the written form, a target re-spell and the
12649        // redacted fingerprint collapse to the canonical bare `JOIN` / `LEFT JOIN`.
12650        let fidelity = honours_source_spelling(ctx);
12651        f.write_str(match self {
12652            // The MySQL `straight` surface tag selects the `STRAIGHT_JOIN` keyword;
12653            // both spellings are the one canonical inner-join shape (ADR-0011).
12654            JoinOperator::Inner { straight: true, .. } => "STRAIGHT_JOIN",
12655            JoinOperator::Inner {
12656                straight: false,
12657                inner: true,
12658                ..
12659            } if fidelity => "INNER JOIN",
12660            JoinOperator::Inner {
12661                straight: false, ..
12662            } => "JOIN",
12663            JoinOperator::LeftOuter { outer: true, .. } if fidelity => "LEFT OUTER JOIN",
12664            JoinOperator::LeftOuter { .. } => "LEFT JOIN",
12665            JoinOperator::RightOuter { outer: true, .. } if fidelity => "RIGHT OUTER JOIN",
12666            JoinOperator::RightOuter { .. } => "RIGHT JOIN",
12667            JoinOperator::FullOuter { outer: true, .. } if fidelity => "FULL OUTER JOIN",
12668            JoinOperator::FullOuter { .. } => "FULL JOIN",
12669            // The canonical ASOF spelling records the side, not the `INNER`/`OUTER`
12670            // noise (like the side joins above).
12671            JoinOperator::AsOf { kind, .. } => match kind {
12672                AsOfJoinKind::Inner => "ASOF JOIN",
12673                AsOfJoinKind::Left => "ASOF LEFT JOIN",
12674                AsOfJoinKind::Right => "ASOF RIGHT JOIN",
12675                AsOfJoinKind::Full => "ASOF FULL JOIN",
12676            },
12677            JoinOperator::Cross { .. } => "CROSS JOIN",
12678            JoinOperator::Positional { .. } => "POSITIONAL JOIN",
12679            // DuckDB's side-less `SEMI`/`ANTI` is the whole `join_type`; its `ASOF`
12680            // composition prefixes the keyword and its `NATURAL` one is emitted by the
12681            // prefix above (the two never co-occur, so `asof` is `false` under NATURAL).
12682            // Spark's sided spelling writes the `LEFT`/`RIGHT` keyword instead and never
12683            // composes with `ASOF`/`NATURAL`, so `asof` is always `false` there.
12684            JoinOperator::Semi { asof, side, .. } => match (side, asof) {
12685                (SemiAntiSide::Sideless, false) => "SEMI JOIN",
12686                (SemiAntiSide::Sideless, true) => "ASOF SEMI JOIN",
12687                (SemiAntiSide::Left, _) => "LEFT SEMI JOIN",
12688                (SemiAntiSide::Right, _) => "RIGHT SEMI JOIN",
12689            },
12690            JoinOperator::Anti { asof, side, .. } => match (side, asof) {
12691                (SemiAntiSide::Sideless, false) => "ANTI JOIN",
12692                (SemiAntiSide::Sideless, true) => "ASOF ANTI JOIN",
12693                (SemiAntiSide::Left, _) => "LEFT ANTI JOIN",
12694                (SemiAntiSide::Right, _) => "RIGHT ANTI JOIN",
12695            },
12696            // The `CROSS`/`OUTER` flavour is the whole operator keyword; the right table
12697            // factor renders after it (no constraint, like CROSS/POSITIONAL above).
12698            JoinOperator::Apply { kind, .. } => match kind {
12699                ApplyKind::Cross => "CROSS APPLY",
12700                ApplyKind::Outer => "OUTER APPLY",
12701            },
12702        })
12703    }
12704}
12705
12706impl<X: Extension + Render> Render for JoinConstraint<X> {
12707    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12708        match self {
12709            JoinConstraint::On { expr, .. } => {
12710                f.write_str(" ON ")?;
12711                expr.render(ctx, f)
12712            }
12713            JoinConstraint::Using { columns, alias, .. } => {
12714                f.write_str(" USING (")?;
12715                render_comma_separated(columns, ctx, f)?;
12716                f.write_str(")")?;
12717                if let Some(alias) = alias {
12718                    f.write_str(" AS ")?;
12719                    alias.render(ctx, f)?;
12720                }
12721                Ok(())
12722            }
12723            // `Natural` is emitted as the keyword prefix; `None` has no clause.
12724            JoinConstraint::Natural { .. } | JoinConstraint::None { .. } => Ok(()),
12725        }
12726    }
12727}
12728
12729// ---------------------------------------------------------------------------
12730// ORDER BY / LIMIT
12731// ---------------------------------------------------------------------------
12732
12733impl<X: Extension + Render> Render for ExtractExpr<X> {
12734    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12735        f.write_str("EXTRACT(")?;
12736        self.field.render(ctx, f)?;
12737        f.write_str(" FROM ")?;
12738        self.source.render(ctx, f)?;
12739        f.write_str(")")
12740    }
12741}
12742
12743impl Render for NullTreatment {
12744    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12745        f.write_str(match self {
12746            NullTreatment::IgnoreNulls => "IGNORE NULLS",
12747            NullTreatment::RespectNulls => "RESPECT NULLS",
12748        })
12749    }
12750}
12751
12752// ---------------------------------------------------------------------------
12753// SQL/JSON expression functions (pg-sqljson-expression-functions)
12754// ---------------------------------------------------------------------------
12755
12756impl Render for JsonEncoding {
12757    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12758        f.write_str(match self {
12759            JsonEncoding::Utf8 => "UTF8",
12760            JsonEncoding::Utf16 => "UTF16",
12761            JsonEncoding::Utf32 => "UTF32",
12762        })
12763    }
12764}
12765
12766impl Render for JsonFormat {
12767    /// Renders `FORMAT JSON [ENCODING <enc>]`; callers write the leading space.
12768    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12769        f.write_str("FORMAT JSON")?;
12770        if let Some(encoding) = &self.encoding {
12771            f.write_str(" ENCODING ")?;
12772            encoding.render(ctx, f)?;
12773        }
12774        Ok(())
12775    }
12776}
12777
12778impl<X: Extension + Render> Render for JsonValueExpr<X> {
12779    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12780        self.expr.render(ctx, f)?;
12781        if let Some(format) = &self.format {
12782            f.write_str(" ")?;
12783            format.render(ctx, f)?;
12784        }
12785        Ok(())
12786    }
12787}
12788
12789impl<X: Extension + Render> Render for JsonReturning<X> {
12790    /// Renders `RETURNING <type> [FORMAT JSON …]`; callers write the leading space.
12791    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12792        f.write_str("RETURNING ")?;
12793        self.data_type.render(ctx, f)?;
12794        if let Some(format) = &self.format {
12795            f.write_str(" ")?;
12796            format.render(ctx, f)?;
12797        }
12798        Ok(())
12799    }
12800}
12801
12802impl<X: Extension + Render> Render for JsonPassingArg<X> {
12803    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12804        self.value.render(ctx, f)?;
12805        f.write_str(" AS ")?;
12806        self.name.render(ctx, f)
12807    }
12808}
12809
12810impl<X: Extension + Render> Render for JsonBehavior<X> {
12811    /// Renders the behaviour keyword(s) only; callers append ` ON EMPTY` / ` ON ERROR`.
12812    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12813        match self.kind {
12814            JsonBehaviorKind::Error => f.write_str("ERROR"),
12815            JsonBehaviorKind::Null => f.write_str("NULL"),
12816            JsonBehaviorKind::True => f.write_str("TRUE"),
12817            JsonBehaviorKind::False => f.write_str("FALSE"),
12818            JsonBehaviorKind::Unknown => f.write_str("UNKNOWN"),
12819            JsonBehaviorKind::Empty => f.write_str("EMPTY"),
12820            JsonBehaviorKind::EmptyArray => f.write_str("EMPTY ARRAY"),
12821            JsonBehaviorKind::EmptyObject => f.write_str("EMPTY OBJECT"),
12822            JsonBehaviorKind::Default => {
12823                f.write_str("DEFAULT ")?;
12824                // `default_expr` is always `Some` when the kind is `Default`; the render
12825                // is a no-op otherwise rather than panicking on a malformed node.
12826                if let Some(expr) = &self.default_expr {
12827                    expr.render(ctx, f)?;
12828                }
12829                Ok(())
12830            }
12831        }
12832    }
12833}
12834
12835/// Render an `ON EMPTY` / `ON ERROR` behaviour, when present: ` <behaviour> ON <slot>`.
12836fn render_json_on_behavior<X: Extension + Render>(
12837    behavior: &Option<JsonBehavior<X>>,
12838    slot: &str,
12839    ctx: &RenderCtx<'_>,
12840    f: &mut fmt::Formatter<'_>,
12841) -> fmt::Result {
12842    if let Some(behavior) = behavior {
12843        f.write_str(" ")?;
12844        behavior.render(ctx, f)?;
12845        f.write_str(" ON ")?;
12846        f.write_str(slot)?;
12847    }
12848    Ok(())
12849}
12850
12851/// Render a SQL/JSON `WRAPPER` clause when specified; callers write no leading space.
12852fn render_json_wrapper(wrapper: JsonWrapperBehavior, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12853    match wrapper {
12854        JsonWrapperBehavior::Unspecified => Ok(()),
12855        JsonWrapperBehavior::Without => f.write_str(" WITHOUT WRAPPER"),
12856        JsonWrapperBehavior::Unconditional => f.write_str(" WITH WRAPPER"),
12857        JsonWrapperBehavior::Conditional => f.write_str(" WITH CONDITIONAL WRAPPER"),
12858    }
12859}
12860
12861/// Render a SQL/JSON `QUOTES` clause when specified; callers write no leading space.
12862fn render_json_quotes(quotes: JsonQuotesBehavior, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12863    match quotes {
12864        JsonQuotesBehavior::Unspecified => Ok(()),
12865        JsonQuotesBehavior::Keep => f.write_str(" KEEP QUOTES"),
12866        JsonQuotesBehavior::Omit => f.write_str(" OMIT QUOTES"),
12867    }
12868}
12869
12870/// Render a null-handling clause when present: ` ABSENT ON NULL` / ` NULL ON NULL`.
12871fn render_json_null_clause(
12872    clause: &Option<JsonNullClause>,
12873    f: &mut fmt::Formatter<'_>,
12874) -> fmt::Result {
12875    match clause {
12876        Some(JsonNullClause::AbsentOnNull) => f.write_str(" ABSENT ON NULL"),
12877        Some(JsonNullClause::NullOnNull) => f.write_str(" NULL ON NULL"),
12878        None => Ok(()),
12879    }
12880}
12881
12882/// Render a key-uniqueness clause when present: ` WITH UNIQUE KEYS` / ` WITHOUT UNIQUE KEYS`.
12883fn render_json_unique(unique_keys: Option<bool>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12884    match unique_keys {
12885        Some(true) => f.write_str(" WITH UNIQUE KEYS"),
12886        Some(false) => f.write_str(" WITHOUT UNIQUE KEYS"),
12887        None => Ok(()),
12888    }
12889}
12890
12891impl<X: Extension + Render> Render for JsonFuncExpr<X> {
12892    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12893        f.write_str(match self.kind {
12894            JsonFuncKind::Value => "JSON_VALUE(",
12895            JsonFuncKind::Query => "JSON_QUERY(",
12896            JsonFuncKind::Exists => "JSON_EXISTS(",
12897        })?;
12898        self.context.render(ctx, f)?;
12899        f.write_str(", ")?;
12900        self.path.render(ctx, f)?;
12901        if !self.passing.is_empty() {
12902            f.write_str(" PASSING ")?;
12903            render_comma_separated(&self.passing, ctx, f)?;
12904        }
12905        if let Some(returning) = &self.returning {
12906            f.write_str(" ")?;
12907            returning.render(ctx, f)?;
12908        }
12909        render_json_wrapper(self.wrapper, f)?;
12910        render_json_quotes(self.quotes, f)?;
12911        render_json_on_behavior(&self.on_empty, "EMPTY", ctx, f)?;
12912        render_json_on_behavior(&self.on_error, "ERROR", ctx, f)?;
12913        f.write_str(")")
12914    }
12915}
12916
12917impl<X: Extension + Render> Render for JsonKeyValue<X> {
12918    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12919        self.key.render(ctx, f)?;
12920        f.write_str(match self.spelling {
12921            JsonKeyValueSpelling::Colon => ": ",
12922            JsonKeyValueSpelling::Value => " VALUE ",
12923        })?;
12924        self.value.render(ctx, f)
12925    }
12926}
12927
12928impl<X: Extension + Render> Render for JsonObjectExpr<X> {
12929    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12930        f.write_str("JSON_OBJECT(")?;
12931        render_comma_separated(&self.entries, ctx, f)?;
12932        render_json_null_clause(&self.null_clause, f)?;
12933        render_json_unique(self.unique_keys, f)?;
12934        if let Some(returning) = &self.returning {
12935            if !self.entries.is_empty() || self.null_clause.is_some() || self.unique_keys.is_some()
12936            {
12937                f.write_str(" ")?;
12938            }
12939            returning.render(ctx, f)?;
12940        }
12941        f.write_str(")")
12942    }
12943}
12944
12945impl<X: Extension + Render> Render for JsonArrayExpr<X> {
12946    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12947        f.write_str("JSON_ARRAY(")?;
12948        let empty_body = match &self.body {
12949            JsonArrayBody::Values {
12950                items, null_clause, ..
12951            } => {
12952                render_comma_separated(items, ctx, f)?;
12953                render_json_null_clause(null_clause, f)?;
12954                items.is_empty() && null_clause.is_none()
12955            }
12956            JsonArrayBody::Query { query, format, .. } => {
12957                query.render(ctx, f)?;
12958                if let Some(format) = format {
12959                    f.write_str(" ")?;
12960                    format.render(ctx, f)?;
12961                }
12962                false
12963            }
12964        };
12965        if let Some(returning) = &self.returning {
12966            if !empty_body {
12967                f.write_str(" ")?;
12968            }
12969            returning.render(ctx, f)?;
12970        }
12971        f.write_str(")")
12972    }
12973}
12974
12975impl<X: Extension + Render> Render for JsonAggregateExpr<X> {
12976    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12977        match &self.body {
12978            JsonAggregateBody::Object {
12979                entry, unique_keys, ..
12980            } => {
12981                f.write_str("JSON_OBJECTAGG(")?;
12982                entry.render(ctx, f)?;
12983                render_json_null_clause(&self.null_clause, f)?;
12984                render_json_unique(*unique_keys, f)?;
12985            }
12986            JsonAggregateBody::Array {
12987                value, order_by, ..
12988            } => {
12989                f.write_str("JSON_ARRAYAGG(")?;
12990                value.render(ctx, f)?;
12991                if !order_by.is_empty() {
12992                    f.write_str(" ORDER BY ")?;
12993                    render_comma_separated(order_by, ctx, f)?;
12994                }
12995                render_json_null_clause(&self.null_clause, f)?;
12996            }
12997        }
12998        if let Some(returning) = &self.returning {
12999            f.write_str(" ")?;
13000            returning.render(ctx, f)?;
13001        }
13002        f.write_str(")")?;
13003        if let Some(filter) = &self.filter {
13004            f.write_str(" FILTER (WHERE ")?;
13005            filter.render(ctx, f)?;
13006            f.write_str(")")?;
13007        }
13008        if let Some(over) = &self.over {
13009            f.write_str(" OVER ")?;
13010            over.render(ctx, f)?;
13011        }
13012        Ok(())
13013    }
13014}
13015
13016impl<X: Extension + Render> Render for JsonConstructorExpr<X> {
13017    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13018        f.write_str(match self.kind {
13019            JsonConstructorKind::Json => "JSON(",
13020            JsonConstructorKind::Scalar => "JSON_SCALAR(",
13021            JsonConstructorKind::Serialize => "JSON_SERIALIZE(",
13022        })?;
13023        self.value.render(ctx, f)?;
13024        render_json_unique(self.unique_keys, f)?;
13025        if let Some(returning) = &self.returning {
13026            f.write_str(" ")?;
13027            returning.render(ctx, f)?;
13028        }
13029        f.write_str(")")
13030    }
13031}
13032
13033impl<X: Extension + Render> Render for IsJsonExpr<X> {
13034    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13035        let full = ctx.mode() == RenderMode::Parenthesized;
13036        open_group(full, f)?;
13037        render_predicate_operand(
13038            &self.expr,
13039            ctx.target().binding_powers.predicate(),
13040            Side::Left,
13041            ctx,
13042            f,
13043        )?;
13044        f.write_str(if self.negated {
13045            " IS NOT JSON"
13046        } else {
13047            " IS JSON"
13048        })?;
13049        f.write_str(match self.item_type {
13050            JsonItemType::Any => "",
13051            JsonItemType::Value => " VALUE",
13052            JsonItemType::Array => " ARRAY",
13053            JsonItemType::Object => " OBJECT",
13054            JsonItemType::Scalar => " SCALAR",
13055        })?;
13056        if self.unique_keys {
13057            f.write_str(" WITH UNIQUE KEYS")?;
13058        }
13059        close_group(full, f)
13060    }
13061}
13062
13063// ---------------------------------------------------------------------------
13064// SQL/XML expression functions (pg-xml-expression-functions)
13065// ---------------------------------------------------------------------------
13066
13067impl Render for XmlDocumentOrContent {
13068    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13069        f.write_str(match self {
13070            XmlDocumentOrContent::Document => "DOCUMENT",
13071            XmlDocumentOrContent::Content => "CONTENT",
13072        })
13073    }
13074}
13075
13076impl<X: Extension + Render> Render for XmlAttribute<X> {
13077    /// Renders `<value> [AS <name>]` — one `xmlattributes` / `xmlforest` element.
13078    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13079        self.value.render(ctx, f)?;
13080        if let Some(name) = &self.name {
13081            f.write_str(" AS ")?;
13082            name.render(ctx, f)?;
13083        }
13084        Ok(())
13085    }
13086}
13087
13088/// Render an optional `xmlexists` passing mechanism: ` BY REF` / ` BY VALUE`.
13089fn render_xml_passing_mechanism(
13090    mechanism: &Option<XmlPassingMechanism>,
13091    f: &mut fmt::Formatter<'_>,
13092) -> fmt::Result {
13093    match mechanism {
13094        Some(XmlPassingMechanism::ByRef) => f.write_str(" BY REF"),
13095        Some(XmlPassingMechanism::ByValue) => f.write_str(" BY VALUE"),
13096        None => Ok(()),
13097    }
13098}
13099
13100impl<X: Extension + Render> Render for XmlFunc<X> {
13101    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13102        match self {
13103            XmlFunc::Element {
13104                name,
13105                attributes,
13106                content,
13107                ..
13108            } => {
13109                f.write_str("xmlelement(NAME ")?;
13110                name.render(ctx, f)?;
13111                if !attributes.is_empty() {
13112                    f.write_str(", xmlattributes(")?;
13113                    render_comma_separated(attributes, ctx, f)?;
13114                    f.write_str(")")?;
13115                }
13116                if !content.is_empty() {
13117                    f.write_str(", ")?;
13118                    render_comma_separated(content, ctx, f)?;
13119                }
13120                f.write_str(")")
13121            }
13122            XmlFunc::Forest { elements, .. } => {
13123                f.write_str("xmlforest(")?;
13124                render_comma_separated(elements, ctx, f)?;
13125                f.write_str(")")
13126            }
13127            XmlFunc::Concat { args, .. } => {
13128                f.write_str("xmlconcat(")?;
13129                render_comma_separated(args, ctx, f)?;
13130                f.write_str(")")
13131            }
13132            XmlFunc::Parse {
13133                option,
13134                arg,
13135                whitespace,
13136                ..
13137            } => {
13138                f.write_str("xmlparse(")?;
13139                option.render(ctx, f)?;
13140                f.write_str(" ")?;
13141                arg.render(ctx, f)?;
13142                match whitespace {
13143                    XmlWhitespaceOption::Unspecified => {}
13144                    XmlWhitespaceOption::Preserve => f.write_str(" PRESERVE WHITESPACE")?,
13145                    XmlWhitespaceOption::Strip => f.write_str(" STRIP WHITESPACE")?,
13146                }
13147                f.write_str(")")
13148            }
13149            XmlFunc::Pi { name, content, .. } => {
13150                f.write_str("xmlpi(NAME ")?;
13151                name.render(ctx, f)?;
13152                if let Some(content) = content {
13153                    f.write_str(", ")?;
13154                    content.render(ctx, f)?;
13155                }
13156                f.write_str(")")
13157            }
13158            XmlFunc::Root {
13159                arg,
13160                version,
13161                standalone,
13162                ..
13163            } => {
13164                f.write_str("xmlroot(")?;
13165                arg.render(ctx, f)?;
13166                f.write_str(", VERSION ")?;
13167                match version {
13168                    Some(expr) => expr.render(ctx, f)?,
13169                    None => f.write_str("NO VALUE")?,
13170                }
13171                match standalone {
13172                    XmlStandalone::Unspecified => {}
13173                    XmlStandalone::Yes => f.write_str(", STANDALONE YES")?,
13174                    XmlStandalone::No => f.write_str(", STANDALONE NO")?,
13175                    XmlStandalone::NoValue => f.write_str(", STANDALONE NO VALUE")?,
13176                }
13177                f.write_str(")")
13178            }
13179            XmlFunc::Serialize {
13180                option,
13181                arg,
13182                data_type,
13183                indent,
13184                ..
13185            } => {
13186                f.write_str("xmlserialize(")?;
13187                option.render(ctx, f)?;
13188                f.write_str(" ")?;
13189                arg.render(ctx, f)?;
13190                f.write_str(" AS ")?;
13191                data_type.render(ctx, f)?;
13192                match indent {
13193                    XmlIndentOption::Unspecified => {}
13194                    XmlIndentOption::Indent => f.write_str(" INDENT")?,
13195                    XmlIndentOption::NoIndent => f.write_str(" NO INDENT")?,
13196                }
13197                f.write_str(")")
13198            }
13199            XmlFunc::Exists {
13200                path,
13201                mechanism_before,
13202                arg,
13203                mechanism_after,
13204                ..
13205            } => {
13206                f.write_str("xmlexists(")?;
13207                path.render(ctx, f)?;
13208                f.write_str(" PASSING")?;
13209                render_xml_passing_mechanism(mechanism_before, f)?;
13210                f.write_str(" ")?;
13211                arg.render(ctx, f)?;
13212                render_xml_passing_mechanism(mechanism_after, f)?;
13213                f.write_str(")")
13214            }
13215        }
13216    }
13217}
13218
13219impl<X: Extension + Render> Render for StringFunc<X> {
13220    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13221        match self {
13222            // The reversed `FOR … FROM …` source order folds onto the same fields, so
13223            // the render is canonically `FROM`-first; a `FOR`-only form keeps its bare
13224            // `FOR` (there is no `FROM` operand to write).
13225            StringFunc::Substring {
13226                expr, start, count, ..
13227            } => {
13228                f.write_str("SUBSTRING(")?;
13229                expr.render(ctx, f)?;
13230                if let Some(start) = start {
13231                    f.write_str(" FROM ")?;
13232                    start.render(ctx, f)?;
13233                }
13234                if let Some(count) = count {
13235                    f.write_str(" FOR ")?;
13236                    count.render(ctx, f)?;
13237                }
13238                f.write_str(")")
13239            }
13240            StringFunc::SubstringSimilar {
13241                expr,
13242                pattern,
13243                escape,
13244                ..
13245            } => {
13246                f.write_str("SUBSTRING(")?;
13247                expr.render(ctx, f)?;
13248                f.write_str(" SIMILAR ")?;
13249                pattern.render(ctx, f)?;
13250                f.write_str(" ESCAPE ")?;
13251                escape.render(ctx, f)?;
13252                f.write_str(")")
13253            }
13254            StringFunc::Position { substr, string, .. } => {
13255                f.write_str("POSITION(")?;
13256                substr.render(ctx, f)?;
13257                f.write_str(" IN ")?;
13258                string.render(ctx, f)?;
13259                f.write_str(")")
13260            }
13261            StringFunc::Overlay {
13262                target,
13263                replacement,
13264                start,
13265                count,
13266                ..
13267            } => {
13268                f.write_str("OVERLAY(")?;
13269                target.render(ctx, f)?;
13270                f.write_str(" PLACING ")?;
13271                replacement.render(ctx, f)?;
13272                f.write_str(" FROM ")?;
13273                start.render(ctx, f)?;
13274                if let Some(count) = count {
13275                    f.write_str(" FOR ")?;
13276                    count.render(ctx, f)?;
13277                }
13278                f.write_str(")")
13279            }
13280            StringFunc::Trim {
13281                side,
13282                trim_chars,
13283                from,
13284                sources,
13285                ..
13286            } => {
13287                f.write_str("TRIM(")?;
13288                if let Some(side) = side {
13289                    side.render(ctx, f)?;
13290                    f.write_str(" ")?;
13291                }
13292                if let Some(trim_chars) = trim_chars {
13293                    trim_chars.render(ctx, f)?;
13294                    f.write_str(" ")?;
13295                }
13296                if *from {
13297                    f.write_str("FROM ")?;
13298                }
13299                render_comma_separated(sources, ctx, f)?;
13300                f.write_str(")")
13301            }
13302            StringFunc::CollationFor { expr, .. } => {
13303                f.write_str("COLLATION FOR (")?;
13304                expr.render(ctx, f)?;
13305                f.write_str(")")
13306            }
13307            StringFunc::ConvertUsing { expr, charset, .. } => {
13308                f.write_str("CONVERT(")?;
13309                expr.render(ctx, f)?;
13310                f.write_str(" USING ")?;
13311                charset.render(ctx, f)?;
13312                f.write_str(")")
13313            }
13314            StringFunc::MatchAgainst {
13315                columns,
13316                against,
13317                modifier,
13318                ..
13319            } => {
13320                f.write_str("MATCH(")?;
13321                render_comma_separated(columns, ctx, f)?;
13322                f.write_str(") AGAINST(")?;
13323                against.render(ctx, f)?;
13324                if let Some(modifier) = modifier {
13325                    f.write_str(" ")?;
13326                    modifier.render(ctx, f)?;
13327                }
13328                f.write_str(")")
13329            }
13330            StringFunc::CeilTo {
13331                expr,
13332                field,
13333                spelling,
13334                ..
13335            } => {
13336                f.write_str(match spelling {
13337                    CeilSpelling::Ceil => "CEIL(",
13338                    CeilSpelling::Ceiling => "CEILING(",
13339                })?;
13340                expr.render(ctx, f)?;
13341                f.write_str(" TO ")?;
13342                field.render(ctx, f)?;
13343                f.write_str(")")
13344            }
13345            StringFunc::FloorTo { expr, field, .. } => {
13346                f.write_str("FLOOR(")?;
13347                expr.render(ctx, f)?;
13348                f.write_str(" TO ")?;
13349                field.render(ctx, f)?;
13350                f.write_str(")")
13351            }
13352        }
13353    }
13354}
13355
13356impl Render for MatchSearchModifier {
13357    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13358        f.write_str(match self {
13359            MatchSearchModifier::NaturalLanguage => "IN NATURAL LANGUAGE MODE",
13360            MatchSearchModifier::NaturalLanguageQueryExpansion => {
13361                "IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION"
13362            }
13363            MatchSearchModifier::Boolean => "IN BOOLEAN MODE",
13364            MatchSearchModifier::QueryExpansion => "WITH QUERY EXPANSION",
13365        })
13366    }
13367}
13368
13369impl Render for TrimSide {
13370    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13371        f.write_str(match self {
13372            TrimSide::Both => "BOTH",
13373            TrimSide::Leading => "LEADING",
13374            TrimSide::Trailing => "TRAILING",
13375        })
13376    }
13377}
13378
13379impl<X: Extension + Render> Render for CaseExpr<X> {
13380    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13381        f.write_str("CASE")?;
13382        if let Some(operand) = &self.operand {
13383            f.write_str(" ")?;
13384            operand.render(ctx, f)?;
13385        }
13386        for clause in &self.when_clauses {
13387            f.write_str(" WHEN ")?;
13388            clause.condition.render(ctx, f)?;
13389            f.write_str(" THEN ")?;
13390            clause.result.render(ctx, f)?;
13391        }
13392        if let Some(else_result) = &self.else_result {
13393            f.write_str(" ELSE ")?;
13394            else_result.render(ctx, f)?;
13395        }
13396        f.write_str(" END")
13397    }
13398}
13399
13400impl<X: Extension + Render> Render for FunctionCall<X> {
13401    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13402        self.name.render(ctx, f)?;
13403        f.write_str("(")?;
13404        if let Some(quantifier) = &self.quantifier {
13405            quantifier.render(ctx, f)?;
13406            f.write_str(" ")?;
13407        }
13408        if self.wildcard {
13409            f.write_str("*")?;
13410        } else {
13411            render_comma_separated(&self.args, ctx, f)?;
13412        }
13413        if !self.order_by.is_empty() {
13414            // DuckDB's standalone form (`rank(ORDER BY x)`) has no positional argument, so
13415            // the `ORDER BY` opens the argument list — no separating space after `(`. A
13416            // preceding wildcard or argument list takes the space; a quantifier already
13417            // rendered its own trailing space.
13418            if self.wildcard || !self.args.is_empty() {
13419                f.write_str(" ")?;
13420            }
13421            f.write_str("ORDER BY ")?;
13422            render_comma_separated(&self.order_by, ctx, f)?;
13423        }
13424        // The MySQL `GROUP_CONCAT` delimiter rides inside the parentheses, after any
13425        // in-parenthesis `ORDER BY`, matching the source order it was parsed in.
13426        if let Some(separator) = &self.separator {
13427            f.write_str(" SEPARATOR ")?;
13428            separator.render(ctx, f)?;
13429        }
13430        // DuckDB's `IGNORE NULLS` / `RESPECT NULLS` null-treatment rides inside the
13431        // parentheses, after any in-parenthesis `ORDER BY` — the position the engine
13432        // accepts (the standard's post-`)` spelling engine-rejects there).
13433        if let Some(null_treatment) = &self.null_treatment {
13434            f.write_str(" ")?;
13435            null_treatment.render(ctx, f)?;
13436        }
13437        f.write_str(")")?;
13438        // MySQL's window-function post-`)` tail rides after the closing parenthesis and
13439        // before `OVER` (and the aggregate clauses MySQL leaves off), in the fixed
13440        // `FROM {FIRST | LAST}` then null-treatment order the grammar admits.
13441        if let Some(tail) = &self.window_tail {
13442            if let Some(from_first_last) = tail.from_first_last {
13443                f.write_str(match from_first_last {
13444                    FromFirstLast::First => " FROM FIRST",
13445                    FromFirstLast::Last => " FROM LAST",
13446                })?;
13447            }
13448            if let Some(null_treatment) = &tail.null_treatment {
13449                f.write_str(" ")?;
13450                null_treatment.render(ctx, f)?;
13451            }
13452        }
13453        // WITHIN GROUP precedes FILTER and OVER, matching PostgreSQL's
13454        // `func_application within_group_clause filter_clause over_clause` order.
13455        if let Some(within_group) = &self.within_group {
13456            f.write_str(" WITHIN GROUP (ORDER BY ")?;
13457            render_comma_separated(within_group, ctx, f)?;
13458            f.write_str(")")?;
13459        }
13460        if let Some(filter) = &self.filter {
13461            // DuckDB round-trips the keyword-less `FILTER (<predicate>)` spelling; every
13462            // other dialect wrote (and re-renders) the standard `WHERE`.
13463            match self.filter_where {
13464                FilterWhereSpelling::Where => f.write_str(" FILTER (WHERE ")?,
13465                FilterWhereSpelling::Omitted => f.write_str(" FILTER (")?,
13466            }
13467            filter.render(ctx, f)?;
13468            f.write_str(")")?;
13469        }
13470        if let Some(over) = &self.over {
13471            f.write_str(" OVER ")?;
13472            over.render(ctx, f)?;
13473        }
13474        Ok(())
13475    }
13476}
13477
13478impl<X: Extension + Render> Render for FunctionArg<X> {
13479    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13480        // The `VARIADIC` array-spread marker prefixes the whole argument, ahead of any
13481        // named-argument arrow (`VARIADIC name => value`). It is a structural keyword, so
13482        // it is emitted in both the plain and the redacted fingerprint modes.
13483        if self.variadic {
13484            f.write_str("VARIADIC ")?;
13485        }
13486        // A named argument prints its `name` and the arrow the source wrote; a
13487        // positional argument carries no name and prints just the value. The name is
13488        // an identifier, so it is masked like one for the redacted fingerprint.
13489        if let Some(name) = self.name {
13490            if ctx.mode() == RenderMode::Redacted {
13491                f.write_str("id")?;
13492            } else {
13493                f.write_str(ctx.resolve(name))?;
13494            }
13495            f.write_str(match self.syntax {
13496                ArgSyntax::ColonEquals => " := ",
13497                // A named argument is never `Positional`; the current `=>` is the
13498                // canonical spelling for the otherwise-unreachable case.
13499                ArgSyntax::Arrow | ArgSyntax::Positional => " => ",
13500            })?;
13501        }
13502        self.value.render(ctx, f)
13503    }
13504}
13505
13506impl<X: Extension + Render> Render for StructField<X> {
13507    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13508        // The key is a field name, so the redacted fingerprint masks it like an
13509        // identifier (collapsing the quote spelling, mirroring `Ident`); otherwise it
13510        // re-emits the source spelling, doubling the embedded delimiter exactly as
13511        // the lexer's doubled-close rule expects so the key round-trips.
13512        if ctx.mode() == RenderMode::Redacted {
13513            f.write_str("id")?;
13514        } else {
13515            let text = ctx.resolve(self.key);
13516            match self.key_spelling {
13517                StructKeySpelling::Bare => f.write_str(text)?,
13518                StructKeySpelling::SingleQuoted => {
13519                    if text.contains('\'') {
13520                        write!(f, "'{}'", text.replace('\'', "''"))?;
13521                    } else {
13522                        write!(f, "'{text}'")?;
13523                    }
13524                }
13525                StructKeySpelling::DoubleQuoted => {
13526                    if text.contains('"') {
13527                        write!(f, "\"{}\"", text.replace('"', "\"\""))?;
13528                    } else {
13529                        write!(f, "\"{text}\"")?;
13530                    }
13531                }
13532            }
13533        }
13534        f.write_str(": ")?;
13535        self.value.render(ctx, f)
13536    }
13537}
13538
13539impl<X: Extension + Render> Render for StructConstructorField<X> {
13540    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13541        if let Some(name) = &self.name {
13542            name.render(ctx, f)?;
13543            f.write_str(" ")?;
13544        }
13545        self.ty.render(ctx, f)
13546    }
13547}
13548
13549impl<X: Extension + Render> Render for StructConstructorArg<X> {
13550    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13551        self.value.render(ctx, f)?;
13552        if let Some(alias) = &self.alias {
13553            f.write_str(" AS ")?;
13554            alias.render(ctx, f)?;
13555        }
13556        Ok(())
13557    }
13558}
13559
13560impl<X: Extension + Render> Render for WindowSpec<X> {
13561    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13562        match self {
13563            WindowSpec::Named { name, .. } => name.render(ctx, f),
13564            WindowSpec::Inline { definition, .. } => {
13565                f.write_str("(")?;
13566                definition.render(ctx, f)?;
13567                f.write_str(")")
13568            }
13569        }
13570    }
13571}
13572
13573impl<X: Extension + Render> Render for WindowDefinition<X> {
13574    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13575        let mut wrote = false;
13576        if let Some(existing) = &self.existing {
13577            existing.render(ctx, f)?;
13578            wrote = true;
13579        }
13580        if !self.partition_by.is_empty() {
13581            if wrote {
13582                f.write_str(" ")?;
13583            }
13584            f.write_str("PARTITION BY ")?;
13585            render_comma_separated(&self.partition_by, ctx, f)?;
13586            wrote = true;
13587        }
13588        if !self.order_by.is_empty() {
13589            if wrote {
13590                f.write_str(" ")?;
13591            }
13592            f.write_str("ORDER BY ")?;
13593            render_comma_separated(&self.order_by, ctx, f)?;
13594            wrote = true;
13595        }
13596        if let Some(frame) = &self.frame {
13597            if wrote {
13598                f.write_str(" ")?;
13599            }
13600            frame.render(ctx, f)?;
13601        }
13602        Ok(())
13603    }
13604}
13605
13606impl<X: Extension + Render> Render for WindowFrame<X> {
13607    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13608        self.units.render(ctx, f)?;
13609        f.write_str(" ")?;
13610        if let Some(end) = &self.end {
13611            f.write_str("BETWEEN ")?;
13612            self.start.render(ctx, f)?;
13613            f.write_str(" AND ")?;
13614            end.render(ctx, f)?;
13615        } else {
13616            self.start.render(ctx, f)?;
13617        }
13618        if let Some(exclusion) = &self.exclusion {
13619            f.write_str(" EXCLUDE ")?;
13620            exclusion.render(ctx, f)?;
13621        }
13622        Ok(())
13623    }
13624}
13625
13626impl Render for WindowFrameUnits {
13627    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13628        f.write_str(match self {
13629            WindowFrameUnits::Rows => "ROWS",
13630            WindowFrameUnits::Range => "RANGE",
13631            WindowFrameUnits::Groups => "GROUPS",
13632        })
13633    }
13634}
13635
13636impl<X: Extension + Render> Render for WindowFrameBound<X> {
13637    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13638        match self {
13639            WindowFrameBound::CurrentRow { .. } => f.write_str("CURRENT ROW"),
13640            WindowFrameBound::UnboundedPreceding { .. } => f.write_str("UNBOUNDED PRECEDING"),
13641            WindowFrameBound::UnboundedFollowing { .. } => f.write_str("UNBOUNDED FOLLOWING"),
13642            WindowFrameBound::Preceding { offset, .. } => {
13643                offset.render(ctx, f)?;
13644                f.write_str(" PRECEDING")
13645            }
13646            WindowFrameBound::Following { offset, .. } => {
13647                offset.render(ctx, f)?;
13648                f.write_str(" FOLLOWING")
13649            }
13650        }
13651    }
13652}
13653
13654impl Render for WindowFrameExclusion {
13655    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13656        f.write_str(match self {
13657            WindowFrameExclusion::CurrentRow => "CURRENT ROW",
13658            WindowFrameExclusion::Group => "GROUP",
13659            WindowFrameExclusion::Ties => "TIES",
13660            WindowFrameExclusion::NoOthers => "NO OTHERS",
13661        })
13662    }
13663}
13664
13665impl<X: Extension + Render> Render for NamedWindow<X> {
13666    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13667        self.name.render(ctx, f)?;
13668        f.write_str(" AS (")?;
13669        self.definition.render(ctx, f)?;
13670        f.write_str(")")
13671    }
13672}
13673
13674impl<X: Extension + Render> Render for OrderByExpr<X> {
13675    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13676        self.expr.render(ctx, f)?;
13677        // `USING <operator>` (PostgreSQL) is mutually exclusive with `ASC`/`DESC`, so
13678        // `asc` is `None` here and `render_sort_direction` emits only the `NULLS`
13679        // suffix that may still follow.
13680        if let Some(using) = &self.using {
13681            f.write_str(" USING ")?;
13682            using.render(ctx, f)?;
13683        }
13684        render_sort_direction(self.asc, self.nulls_first, f)
13685    }
13686}
13687
13688impl Render for OrderByUsing {
13689    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13690        // A schema-qualified operator must round-trip through the explicit
13691        // `OPERATOR(schema.op)` spelling; a bare operator renders symbolically
13692        // (which also canonicalizes an unqualified `OPERATOR(<)` to bare `<`,
13693        // the same operator).
13694        let Some(schema) = &self.schema else {
13695            return f.write_str(ctx.resolve(self.op));
13696        };
13697        f.write_str("OPERATOR(")?;
13698        for part in &schema.0 {
13699            part.render(ctx, f)?;
13700            f.write_str(".")?;
13701        }
13702        f.write_str(ctx.resolve(self.op))?;
13703        f.write_str(")")
13704    }
13705}
13706
13707impl Render for OrderByAll {
13708    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13709        f.write_str("ALL")?;
13710        render_sort_direction(self.asc, self.nulls_first, f)
13711    }
13712}
13713
13714impl<X: Extension + Render> Render for Limit<X> {
13715    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13716        match self.syntax {
13717            // MySQL/SQLite `LIMIT <offset>, <count>`: a source-fidelity render replays
13718            // the comma spelling; a re-spell and the redacted fingerprint fall through
13719            // to the canonical `LIMIT <count> OFFSET <offset>` below.
13720            LimitSyntax::CommaOffset if honours_source_spelling(ctx) => {
13721                f.write_str("LIMIT ")?;
13722                if let Some(offset) = &self.offset {
13723                    offset.render(ctx, f)?;
13724                }
13725                f.write_str(", ")?;
13726                if let Some(limit) = &self.limit {
13727                    limit.render(ctx, f)?;
13728                }
13729                Ok(())
13730            }
13731            LimitSyntax::LimitOffset | LimitSyntax::CommaOffset => {
13732                let mut wrote = false;
13733                if let Some(limit) = &self.limit {
13734                    f.write_str("LIMIT ")?;
13735                    // DuckDB percentage limit: re-emit the written marker (`LIMIT 40 PERCENT`
13736                    // / `LIMIT 35%`). `None` is the ordinary row count. The `%` spelling
13737                    // renders with no separating space (`LIMIT 20 %` canonicalizes onto it).
13738                    // The marker reduces onto a multiplicative-or-tighter operand, so a
13739                    // count carrying a looser binary operator is re-parenthesized to reparse
13740                    // as the percentage count rather than re-associating with a `%` read as
13741                    // modulo (`LIMIT (30-10)%`, not `LIMIT 30 - 10%`) — mirroring the parse
13742                    // threshold in `parse_limit_percent_operand` (`Side::Right` of the
13743                    // multiplicative rank). A `PERCENT`-keyword count is always a literal, so
13744                    // the wrap is a no-op there.
13745                    match self.percent {
13746                        Some(marker) => {
13747                            render_pg_operand(
13748                                limit,
13749                                ctx.target().binding_powers.multiplicative,
13750                                Side::Right,
13751                                ctx,
13752                                f,
13753                            )?;
13754                            f.write_str(match marker {
13755                                LimitPercent::Symbol => "%",
13756                                LimitPercent::Keyword => " PERCENT",
13757                            })?;
13758                        }
13759                        None => limit.render(ctx, f)?,
13760                    }
13761                    wrote = true;
13762                }
13763                if let Some(offset) = &self.offset {
13764                    if wrote {
13765                        f.write_str(" ")?;
13766                    }
13767                    f.write_str("OFFSET ")?;
13768                    offset.render(ctx, f)?;
13769                }
13770                Ok(())
13771            }
13772            LimitSyntax::FetchFirst => {
13773                // `FIRST`/`NEXT` and `ROW`/`ROWS` are interchangeable surface noise: a
13774                // source-fidelity render replays the written pair (`fetch_spelling`), a
13775                // re-spell and the redacted fingerprint keep the canonical `FIRST` /
13776                // `ROWS`. The `OFFSET … ROWS` word always renders plural (its rare
13777                // singular form is not tagged — it never crossed the sweep).
13778                let fidelity = honours_source_spelling(ctx);
13779                let mut wrote = false;
13780                if let Some(offset) = &self.offset {
13781                    f.write_str("OFFSET ")?;
13782                    offset.render(ctx, f)?;
13783                    f.write_str(" ROWS")?;
13784                    wrote = true;
13785                }
13786                // `with_ties: Some(_)` is the signal that a `FETCH` tail was
13787                // actually written (see `Limit::with_ties`'s doc comment) — the
13788                // count itself is optional (`FETCH FIRST ROWS ONLY`), so `limit`
13789                // alone cannot carry that.
13790                if let Some(with_ties) = self.with_ties {
13791                    if wrote {
13792                        f.write_str(" ")?;
13793                    }
13794                    let spelling = if fidelity {
13795                        self.fetch_spelling
13796                    } else {
13797                        FetchSpelling::FirstRows
13798                    };
13799                    f.write_str(spelling.fetch_keyword())?;
13800                    if let Some(limit) = &self.limit {
13801                        f.write_str(" ")?;
13802                        limit.render(ctx, f)?;
13803                    }
13804                    f.write_str(spelling.row_word())?;
13805                    f.write_str(if with_ties { "WITH TIES" } else { "ONLY" })?;
13806                }
13807                Ok(())
13808            }
13809        }
13810    }
13811}
13812
13813impl<X: Extension + Render> Render for LimitBy<X> {
13814    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13815        f.write_str("LIMIT ")?;
13816        self.limit.render(ctx, f)?;
13817        if let Some(offset) = &self.offset {
13818            f.write_str(" OFFSET ")?;
13819            offset.render(ctx, f)?;
13820        }
13821        f.write_str(" BY ")?;
13822        render_comma_separated(&self.by, ctx, f)
13823    }
13824}
13825
13826impl<X: Extension + Render> Render for Setting<X> {
13827    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13828        self.name.render(ctx, f)?;
13829        f.write_str(" = ")?;
13830        self.value.render(ctx, f)
13831    }
13832}
13833
13834impl Render for FormatClause {
13835    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13836        f.write_str("FORMAT ")?;
13837        self.name.render(ctx, f)
13838    }
13839}
13840
13841impl Render for ForClause {
13842    /// Renders the MSSQL `FOR XML`/`FOR JSON` tail with the directives in the canonical
13843    /// MSSQL order (parse accepts them order-independently). Callers write the leading
13844    /// space.
13845    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13846        match self {
13847            ForClause::Xml {
13848                mode,
13849                elements,
13850                binary_base64,
13851                typed,
13852                root,
13853                ..
13854            } => {
13855                f.write_str("FOR XML ")?;
13856                mode.render(ctx, f)?;
13857                if *binary_base64 {
13858                    f.write_str(", BINARY BASE64")?;
13859                }
13860                if *typed {
13861                    f.write_str(", TYPE")?;
13862                }
13863                if let Some(root) = root {
13864                    f.write_str(", ")?;
13865                    root.render(ctx, f)?;
13866                }
13867                if let Some(elements) = elements {
13868                    f.write_str(", ")?;
13869                    elements.render(ctx, f)?;
13870                }
13871                Ok(())
13872            }
13873            ForClause::Json {
13874                mode,
13875                root,
13876                include_null_values,
13877                without_array_wrapper,
13878                ..
13879            } => {
13880                f.write_str("FOR JSON ")?;
13881                mode.render(ctx, f)?;
13882                if let Some(root) = root {
13883                    f.write_str(", ")?;
13884                    root.render(ctx, f)?;
13885                }
13886                if *include_null_values {
13887                    f.write_str(", INCLUDE_NULL_VALUES")?;
13888                }
13889                if *without_array_wrapper {
13890                    f.write_str(", WITHOUT_ARRAY_WRAPPER")?;
13891                }
13892                Ok(())
13893            }
13894        }
13895    }
13896}
13897
13898impl Render for ForXmlMode {
13899    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13900        match self {
13901            ForXmlMode::Raw { name, .. } => {
13902                f.write_str("RAW")?;
13903                render_optional_for_name(name.as_ref(), ctx, f)
13904            }
13905            ForXmlMode::Auto { .. } => f.write_str("AUTO"),
13906            ForXmlMode::Explicit { .. } => f.write_str("EXPLICIT"),
13907            ForXmlMode::Path { name, .. } => {
13908                f.write_str("PATH")?;
13909                render_optional_for_name(name.as_ref(), ctx, f)
13910            }
13911        }
13912    }
13913}
13914
13915impl Render for ForXmlElements {
13916    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13917        f.write_str(match self {
13918            ForXmlElements::Plain => "ELEMENTS",
13919            ForXmlElements::XsiNil => "ELEMENTS XSINIL",
13920            ForXmlElements::Absent => "ELEMENTS ABSENT",
13921        })
13922    }
13923}
13924
13925impl Render for ForJsonMode {
13926    fn render(&self, _ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13927        f.write_str(match self {
13928            ForJsonMode::Auto => "AUTO",
13929            ForJsonMode::Path => "PATH",
13930        })
13931    }
13932}
13933
13934impl Render for ForRoot {
13935    fn render(&self, ctx: &RenderCtx<'_>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13936        f.write_str("ROOT")?;
13937        render_optional_for_name(self.name.as_ref(), ctx, f)
13938    }
13939}
13940
13941/// Render an optional `('name')` element/root name (`RAW`/`PATH`/`ROOT`); a no-op when
13942/// the name is absent.
13943fn render_optional_for_name(
13944    name: Option<&Literal>,
13945    ctx: &RenderCtx<'_>,
13946    f: &mut fmt::Formatter<'_>,
13947) -> fmt::Result {
13948    if let Some(name) = name {
13949        f.write_str("(")?;
13950        name.render(ctx, f)?;
13951        f.write_str(")")?;
13952    }
13953    Ok(())
13954}