prettier-plugin-tsql 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { keyword, hardline, join, indent, group, softline, line, ifExistsDoc, commentsBlock, parenList } from '@prettier-sql/core/printer/utils';
1
+ import { keyword, hardline, join, indent, group, softline, line, ifExistsDoc, commentsBlock, parenList, } from '@prettier-sql/core/printer/utils';
2
2
  import { prop, propArr, propStr, propBool, schemaObjectName } from './helpers.js';
3
3
  // printNode / printBool / qexpr / printStatementWithComments are imported from statements.ts
4
4
  // — circular but safe in ESM (all imports are function references, never accessed during init)
@@ -6,6 +6,18 @@ import { printStatementWithComments, printNode, printBool, qexpr } from './state
6
6
  // ---------------------------------------------------------------------------
7
7
  // Shared helpers
8
8
  // ---------------------------------------------------------------------------
9
+ /**
10
+ * When ScriptDOM sees `AS BEGIN...END` it wraps the body in a single
11
+ * BeginEndBlockStatement. Unwrap that one outer block so that proc/function/
12
+ * trigger printers can emit their own BEGIN/END delimiters cleanly, while
13
+ * a *standalone* BEGIN...END statement still prints with its own delimiters.
14
+ */
15
+ function unwrapBodyBlock(stmts) {
16
+ if (stmts.length === 1 && stmts[0]?.type === 'BeginEndBlock') {
17
+ return propArr(stmts[0], 'statements');
18
+ }
19
+ return stmts;
20
+ }
9
21
  /** Render ` NULL` / ` NOT NULL` from a tristate `nullable` prop value. */
10
22
  function nullablePart(nullable, opts) {
11
23
  if (nullable === true)
@@ -33,22 +45,77 @@ function withOptionsClause(options, opts) {
33
45
  // ---------------------------------------------------------------------------
34
46
  // CREATE TABLE
35
47
  // ---------------------------------------------------------------------------
48
+ /** Inline INDEX definition within CREATE TABLE body. */
49
+ function printInlineIndex(node, opts) {
50
+ const indexName = propStr(node, 'indexName') ?? '';
51
+ const isUnique = node.props?.['unique'];
52
+ const kind = propStr(node, 'kind'); // 'clustered', 'nonclustered', etc.
53
+ const columns = propArr(node, 'columns');
54
+ const includeColumns = node.props?.['includeColumns'];
55
+ const filterPredicate = propStr(node, 'filterPredicate');
56
+ const indexOptions = node.props?.['indexOptions'];
57
+ const uniqueKw = isUnique ? [keyword('UNIQUE', opts), ' '] : '';
58
+ const kindKw = kind ? [keyword(kind.toUpperCase(), opts), ' '] : '';
59
+ const colDocs = columns.map((c) => {
60
+ const colName = propStr(c, 'name') ?? '';
61
+ const sort = propStr(c, 'sortOrder') ?? 'Ascending';
62
+ return sort === 'Descending' ? [colName, ' ', keyword('DESC', opts)] : [colName, ' ', keyword('ASC', opts)];
63
+ });
64
+ const includePart = includeColumns?.length
65
+ ? [' ', keyword('INCLUDE', opts), ' ', parenList(includeColumns)]
66
+ : '';
67
+ const filterPart = filterPredicate ? [' ', keyword('WHERE', opts), ' ', filterPredicate] : '';
68
+ const withPart = indexOptions?.length ? [' ', keyword('WITH', opts), ' (', join(', ', indexOptions), ')'] : '';
69
+ return [
70
+ keyword('INDEX', opts),
71
+ ' ',
72
+ indexName,
73
+ ' ',
74
+ uniqueKw,
75
+ kindKw,
76
+ parenList(colDocs),
77
+ includePart,
78
+ filterPart,
79
+ withPart,
80
+ ];
81
+ }
36
82
  export function printCreateTable(node, opts) {
37
83
  const columns = propArr(node, 'columns');
38
84
  const constraints = propArr(node, 'constraints');
39
85
  const options = node.props?.['options'];
86
+ const systemTimePeriod = node.props?.['systemTimePeriod'];
87
+ const indexes = propArr(node, 'indexes');
40
88
  const allDefs = [
41
89
  ...columns.map((col) => printColumnDef(col, opts)),
42
90
  ...constraints.map((c) => printConstraintDef(c, opts)),
91
+ ...indexes.map((idx) => printInlineIndex(idx, opts)),
43
92
  ];
44
- const withPart = options && options.length > 0
45
- ? [
46
- hardline,
47
- keyword('WITH', opts),
48
- ' ',
49
- parenList(options),
50
- ]
51
- : '';
93
+ // PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo) always last in the table body
94
+ if (systemTimePeriod) {
95
+ allDefs.push([
96
+ keyword('PERIOD FOR SYSTEM_TIME', opts),
97
+ ' (',
98
+ systemTimePeriod.startColumn,
99
+ ', ',
100
+ systemTimePeriod.endColumn,
101
+ ')',
102
+ ]);
103
+ }
104
+ const withPart = options && options.length > 0 ? [hardline, keyword('WITH', opts), ' ', parenList(options)] : '';
105
+ const onFileGroup = propStr(node, 'onFileGroup');
106
+ const textimageOn = propStr(node, 'textimageOn');
107
+ const fileStreamOn = propStr(node, 'fileStreamOn');
108
+ const onPart = onFileGroup ? [hardline, keyword('ON', opts), ' ', onFileGroup] : '';
109
+ const textimagePart = textimageOn ? [hardline, keyword('TEXTIMAGE_ON', opts), ' ', textimageOn] : '';
110
+ const fileStreamPart = fileStreamOn ? [hardline, keyword('FILESTREAM_ON', opts), ' ', fileStreamOn] : '';
111
+ // Graph table types (AS NODE / AS EDGE)
112
+ const asNode = node.props?.['asNode'];
113
+ const asEdge = node.props?.['asEdge'];
114
+ const graphPart = asNode
115
+ ? [' ', keyword('AS NODE', opts)]
116
+ : asEdge
117
+ ? [' ', keyword('AS EDGE', opts)]
118
+ : '';
52
119
  return group([
53
120
  keyword('CREATE TABLE', opts),
54
121
  ' ',
@@ -57,16 +124,27 @@ export function printCreateTable(node, opts) {
57
124
  indent([hardline, join([',', hardline], allDefs)]),
58
125
  hardline,
59
126
  ')',
127
+ graphPart,
128
+ onPart,
129
+ fileStreamPart,
130
+ textimagePart,
60
131
  withPart,
61
132
  ';',
62
133
  ]);
63
134
  }
64
135
  export function printColumnDef(node, opts) {
136
+ // Raw leaf (e.g. ENCRYPTED WITH — property names vary across ScriptDOM versions).
137
+ // The C# AstBuilder emits `Leaf("ColumnDefinition", col, rawText)` which sets
138
+ // `node.text` to the original column fragment and leaves `node.props` undefined.
139
+ if (!node.props)
140
+ return node.text ?? '/* column */';
65
141
  const name = propStr(node, 'name') ?? 'col';
66
- // Computed column: Name AS expression [PERSISTED]
142
+ // Computed column: Name AS expression [PERSISTED] [NOT NULL|NULL]
67
143
  const computedExpr = prop(node, 'computedExpression');
68
144
  if (computedExpr) {
69
145
  const isPersisted = node.props?.['isPersisted'];
146
+ // Computed PERSISTED columns may have an explicit nullability constraint
147
+ const computedNullPart = nullablePart(node.props?.['nullable'], opts);
70
148
  return [
71
149
  name,
72
150
  ' ',
@@ -74,28 +152,124 @@ export function printColumnDef(node, opts) {
74
152
  ' ',
75
153
  printNode(computedExpr, opts),
76
154
  isPersisted ? [' ', keyword('PERSISTED', opts)] : '',
155
+ computedNullPart,
77
156
  ];
78
157
  }
79
158
  const dataType = propStr(node, 'dataType') ?? 'INT';
80
159
  const params = node.props?.['dataTypeParams'];
160
+ const xmlSchemaCollection = propStr(node, 'xmlSchemaCollection');
161
+ const xmlTypeOption = propStr(node, 'xmlTypeOption');
81
162
  // Read nullable as a tristate (true/false/undefined) — propBool only returns true/false.
82
163
  const isNullable = node.props?.['nullable'];
83
164
  const isIdentity = propBool(node, 'identity');
84
165
  const identitySeed = propStr(node, 'identitySeed');
85
166
  const identityIncrement = propStr(node, 'identityIncrement');
86
167
  const defaultValue = prop(node, 'defaultValue');
87
- const typeStr = Array.isArray(params) && params.length > 0
88
- ? [keyword(dataType, opts), `(${params.join(', ')})`]
89
- : keyword(dataType, opts);
168
+ const checkConstraint = prop(node, 'checkConstraint');
169
+ const collation = propStr(node, 'collation');
170
+ const typeStr = (() => {
171
+ const baseType = keyword(dataType, opts);
172
+ if (Array.isArray(params) && params.length > 0) {
173
+ return [baseType, `(${params.join(', ')})`];
174
+ }
175
+ if (xmlSchemaCollection) {
176
+ // xml(CONTENT|DOCUMENT schema_collection) — CONTENT/DOCUMENT are optional keywords
177
+ const prefix = xmlTypeOption ? `${keyword(xmlTypeOption, opts)} ` : '';
178
+ return [baseType, '(', prefix, xmlSchemaCollection, ')'];
179
+ }
180
+ return baseType;
181
+ })();
90
182
  const parts = [name, ' ', typeStr];
183
+ // COLLATE clause comes right after the data type
184
+ if (collation)
185
+ parts.push(' ', keyword('COLLATE', opts), ' ', collation);
91
186
  if (isIdentity) {
92
187
  const seed = identitySeed ?? '1';
93
188
  const inc = identityIncrement ?? '1';
94
189
  parts.push(' ', keyword('IDENTITY', opts), `(${seed}, ${inc})`);
190
+ if (node.props?.['identityNotForReplication'])
191
+ parts.push(' ', keyword('NOT FOR REPLICATION', opts));
192
+ }
193
+ if (node.props?.['isRowGuidCol'])
194
+ parts.push(' ', keyword('ROWGUIDCOL', opts));
195
+ // SPARSE / FILESTREAM / COLUMN_SET
196
+ if (node.props?.['isSparse'])
197
+ parts.push(' ', keyword('SPARSE', opts));
198
+ if (node.props?.['isFileStream'])
199
+ parts.push(' ', keyword('FILESTREAM', opts));
200
+ if (node.props?.['isColumnSet'])
201
+ parts.push(' ', keyword('COLUMN_SET FOR ALL_SPARSE_COLUMNS', opts));
202
+ // Temporal table: GENERATED ALWAYS AS ROW START / ROW END [HIDDEN]
203
+ const generatedAlways = propStr(node, 'generatedAlways');
204
+ if (generatedAlways) {
205
+ const gaMap = {
206
+ RowStart: 'ROW START',
207
+ RowEnd: 'ROW END',
208
+ UserIdStart: 'USER ID START',
209
+ UserIdEnd: 'USER ID END',
210
+ UserNameStart: 'USER NAME START',
211
+ UserNameEnd: 'USER NAME END',
212
+ TransactionIdStart: 'TRANSACTION ID START',
213
+ TransactionIdEnd: 'TRANSACTION ID END',
214
+ SequenceNumberStart: 'SEQUENCE NUMBER START',
215
+ SequenceNumberEnd: 'SEQUENCE NUMBER END',
216
+ };
217
+ const gaKw = gaMap[generatedAlways] ?? generatedAlways.toUpperCase();
218
+ parts.push(' ', keyword('GENERATED ALWAYS AS', opts), ' ', keyword(gaKw, opts));
219
+ }
220
+ if (node.props?.['isHidden'])
221
+ parts.push(' ', keyword('HIDDEN', opts));
222
+ // Dynamic data masking
223
+ if (node.props?.['isMasked']) {
224
+ const maskFn = propStr(node, 'maskingFunction') ?? 'default()';
225
+ parts.push(' ', keyword('MASKED WITH', opts), ' (', keyword('FUNCTION', opts), ` = '${maskFn}')`);
226
+ }
227
+ if (defaultValue) {
228
+ const defaultName = propStr(node, 'defaultConstraintName');
229
+ const defaultNamePrefix = defaultName ? [keyword('CONSTRAINT', opts), ' ', defaultName, ' '] : '';
230
+ parts.push(' ', defaultNamePrefix, keyword('DEFAULT', opts), ' ', printNode(defaultValue, opts));
95
231
  }
96
- if (defaultValue)
97
- parts.push(' ', keyword('DEFAULT', opts), ' ', printNode(defaultValue, opts));
98
232
  parts.push(nullablePart(isNullable, opts));
233
+ if (checkConstraint) {
234
+ const checkName = propStr(node, 'checkConstraintName');
235
+ const checkPrefix = checkName ? [keyword('CONSTRAINT', opts), ' ', checkName, ' '] : '';
236
+ parts.push(' ', checkPrefix, keyword('CHECK', opts), ' (', printBool(checkConstraint, opts), ')');
237
+ }
238
+ // Inline PRIMARY KEY / UNIQUE constraint (e.g. in table variable declarations)
239
+ const uniqueConstraint = node.props?.['uniqueConstraint'];
240
+ if (uniqueConstraint) {
241
+ const constraintNamePrefix = uniqueConstraint.constraintName
242
+ ? [keyword('CONSTRAINT', opts), ' ', uniqueConstraint.constraintName, ' ']
243
+ : '';
244
+ const uqKw = uniqueConstraint.isPrimaryKey ? keyword('PRIMARY KEY', opts) : keyword('UNIQUE', opts);
245
+ const clusteredKw = uniqueConstraint.clustered === true
246
+ ? [' ', keyword('CLUSTERED', opts)]
247
+ : uniqueConstraint.clustered === false
248
+ ? [' ', keyword('NONCLUSTERED', opts)]
249
+ : '';
250
+ parts.push(' ', constraintNamePrefix, uqKw, clusteredKw);
251
+ }
252
+ // Inline REFERENCES (column-level foreign key: col type [CONSTRAINT name] REFERENCES Table(col))
253
+ const foreignKey = node.props?.['foreignKey'];
254
+ if (foreignKey) {
255
+ if (foreignKey.constraintName) {
256
+ parts.push(' ', keyword('CONSTRAINT', opts), ' ', foreignKey.constraintName);
257
+ }
258
+ const refColsPart = foreignKey.refColumns?.length ? [' (', join(', ', foreignKey.refColumns), ')'] : '';
259
+ parts.push(' ', keyword('REFERENCES', opts), ' ', schemaObjectName(foreignKey.refTable), refColsPart);
260
+ if (foreignKey.deleteAction) {
261
+ parts.push(' ', keyword('ON DELETE', opts), ' ', keyword(foreignKey.deleteAction
262
+ .replace(/([A-Z])/g, ' $1')
263
+ .trim()
264
+ .toUpperCase(), opts));
265
+ }
266
+ if (foreignKey.updateAction) {
267
+ parts.push(' ', keyword('ON UPDATE', opts), ' ', keyword(foreignKey.updateAction
268
+ .replace(/([A-Z])/g, ' $1')
269
+ .trim()
270
+ .toUpperCase(), opts));
271
+ }
272
+ }
99
273
  return parts;
100
274
  }
101
275
  export function printConstraintDef(node, opts) {
@@ -104,14 +278,40 @@ export function printConstraintDef(node, opts) {
104
278
  switch (node.type) {
105
279
  case 'UniqueConstraint': {
106
280
  const isPK = propBool(node, 'isPrimaryKey');
107
- const cols = Array.isArray(node.props?.['columns']) ? node.props?.['columns'] : [];
281
+ const clustered = node.props?.['clustered'];
282
+ // Only emit CLUSTERED/NONCLUSTERED when explicitly specified in DDL
283
+ const clusteredKw = clustered === true
284
+ ? [keyword('CLUSTERED', opts), ' ']
285
+ : clustered === false
286
+ ? [keyword('NONCLUSTERED', opts), ' ']
287
+ : '';
108
288
  const kw = isPK ? keyword('PRIMARY KEY', opts) : keyword('UNIQUE', opts);
109
- const colsDoc = parenList(cols);
110
- return group([namePrefix, indent([softline, kw, ' ', colsDoc])]);
289
+ // Columns are now {name, order} objects; fall back to plain strings for compat
290
+ const rawCols = Array.isArray(node.props?.['columns']) ? node.props['columns'] : [];
291
+ const colDocs = rawCols.map((c) => {
292
+ if (typeof c === 'string')
293
+ return c;
294
+ const dir = c.order === 'Descending' ? [' ', keyword('DESC', opts)] : '';
295
+ return [c.name, dir];
296
+ });
297
+ const colsDoc = parenList(colDocs);
298
+ const indexOptions = node.props?.['indexOptions'];
299
+ const withPart = indexOptions?.length
300
+ ? [' ', keyword('WITH', opts), ' (', join(', ', indexOptions), ')']
301
+ : '';
302
+ return group([namePrefix, indent([softline, kw, ' ', clusteredKw, colsDoc]), withPart]);
111
303
  }
112
304
  case 'CheckConstraint': {
113
305
  const expr = prop(node, 'expression');
114
- return [namePrefix, keyword('CHECK', opts), ' (', expr ? printBool(expr, opts) : '', ')'];
306
+ const nfr = propBool(node, 'notForReplication');
307
+ return [
308
+ namePrefix,
309
+ keyword('CHECK', opts),
310
+ nfr ? [' ', keyword('NOT FOR REPLICATION', opts)] : '',
311
+ ' (',
312
+ expr ? printBool(expr, opts) : '',
313
+ ')',
314
+ ];
115
315
  }
116
316
  case 'ForeignKeyConstraint': {
117
317
  const cols = Array.isArray(node.props?.['columns']) ? node.props?.['columns'] : [];
@@ -120,6 +320,7 @@ export function printConstraintDef(node, opts) {
120
320
  const refName = refTable ? schemaObjectName(refTable) : '';
121
321
  const deleteAction = propStr(node, 'deleteAction');
122
322
  const updateAction = propStr(node, 'updateAction');
323
+ const nfr = propBool(node, 'notForReplication');
123
324
  const refActionKw = (action) => keyword(action
124
325
  .replace(/([A-Z])/g, ' $1')
125
326
  .trim()
@@ -139,6 +340,7 @@ export function printConstraintDef(node, opts) {
139
340
  ]),
140
341
  deleteAction ? [line, keyword('ON DELETE', opts), ' ', refActionKw(deleteAction)] : '',
141
342
  updateAction ? [line, keyword('ON UPDATE', opts), ' ', refActionKw(updateAction)] : '',
343
+ nfr ? [line, keyword('NOT FOR REPLICATION', opts)] : '',
142
344
  ]),
143
345
  ]),
144
346
  ];
@@ -179,6 +381,11 @@ export function printAlterTable(node, opts) {
179
381
  join([',', line], elements.map((e) => e.name)),
180
382
  ]),
181
383
  ]);
384
+ // WITH (ONLINE = ON, WAIT_AT_LOW_PRIORITY ...) on DROP CLUSTERED CONSTRAINT
385
+ const allDropOptions = elements.flatMap((e) => e.dropOptions ?? []);
386
+ const withPart = allDropOptions.length
387
+ ? [' ', keyword('WITH', opts), ' (', join(', ', allDropOptions), ')']
388
+ : '';
182
389
  return [
183
390
  keyword('ALTER TABLE', opts),
184
391
  ' ',
@@ -188,6 +395,7 @@ export function printAlterTable(node, opts) {
188
395
  ifExists ? [' ', keyword('IF EXISTS', opts)] : '',
189
396
  ' ',
190
397
  nameList,
398
+ withPart,
191
399
  ';',
192
400
  ];
193
401
  }
@@ -213,7 +421,49 @@ export function printAlterTable(node, opts) {
213
421
  }
214
422
  if (alterType === 'AlterTableAlterColumnStatement') {
215
423
  const column = propStr(node, 'column') ?? '';
424
+ const alterColumnOption = propStr(node, 'alterColumnOption');
425
+ const maskingFunction = propStr(node, 'maskingFunction');
426
+ // ADD/DROP modifier variants (no data type change, just add/remove a column property)
427
+ if (alterColumnOption) {
428
+ let optDoc;
429
+ if (alterColumnOption === 'AddMaskingFunction') {
430
+ // ALTER COLUMN col ADD MASKED WITH (FUNCTION = 'fn()')
431
+ const fn = maskingFunction ?? 'default()';
432
+ optDoc = [keyword('ADD MASKED WITH', opts), ' (', keyword('FUNCTION', opts), ` = '${fn}')`];
433
+ }
434
+ else {
435
+ const optMap = {
436
+ DropMaskingFunction: 'DROP MASKED',
437
+ AddSparse: 'ADD SPARSE',
438
+ DropSparse: 'DROP SPARSE',
439
+ AddRowGuidCol: 'ADD ROWGUIDCOL',
440
+ DropRowGuidCol: 'DROP ROWGUIDCOL',
441
+ AddHidden: 'ADD HIDDEN',
442
+ DropHidden: 'DROP HIDDEN',
443
+ AddPersisted: 'ADD PERSISTED',
444
+ DropPersisted: 'DROP PERSISTED',
445
+ };
446
+ optDoc = keyword(optMap[alterColumnOption] ?? alterColumnOption.toUpperCase(), opts);
447
+ }
448
+ return [
449
+ keyword('ALTER TABLE', opts),
450
+ ' ',
451
+ name,
452
+ hardline,
453
+ keyword('ALTER COLUMN', opts),
454
+ ' ',
455
+ column,
456
+ ' ',
457
+ optDoc,
458
+ ';',
459
+ ];
460
+ }
461
+ // Normal type-change: ALTER COLUMN col newtype [COLLATE ...] [NULL|NOT NULL]
216
462
  const dataType = propStr(node, 'dataType') ?? '';
463
+ const collationAC = propStr(node, 'collation');
464
+ const collatePart = collationAC
465
+ ? [' ', keyword('COLLATE', opts), ' ', collationAC]
466
+ : '';
217
467
  const nullPart = nullablePart(node.props?.['nullable'], opts);
218
468
  return [
219
469
  keyword('ALTER TABLE', opts),
@@ -225,18 +475,16 @@ export function printAlterTable(node, opts) {
225
475
  column,
226
476
  ' ',
227
477
  keyword(dataType, opts),
478
+ collatePart,
228
479
  nullPart,
229
480
  ';',
230
481
  ];
231
482
  }
232
483
  if (alterType === 'AlterTableSetStatement') {
233
- // Convert camelCase enum names to SQL_KEYWORD style: LockEscalation → LOCK_ESCALATION
234
- const toSqlKw = (s) => s
235
- .replace(/([A-Z])/g, '_$1')
236
- .replace(/^_/, '')
237
- .toUpperCase();
484
+ // Options come pre-serialized from SerializeTableOption (e.g. "lock_escalation = table",
485
+ // "system_versioning = on (history_table = dbo.Tbl)"). Render them verbatim — applying
486
+ // keyword() casing would uppercase embedded schema/table names.
238
487
  const options = (node.props?.['options'] ?? []);
239
- const optDocs = options.map((o) => [keyword(toSqlKw(o.kind), opts), ' = ', keyword(toSqlKw(o.value), opts)]);
240
488
  return [
241
489
  keyword('ALTER TABLE', opts),
242
490
  ' ',
@@ -244,7 +492,7 @@ export function printAlterTable(node, opts) {
244
492
  hardline,
245
493
  keyword('SET', opts),
246
494
  ' (',
247
- join(', ', optDocs),
495
+ join(', ', options),
248
496
  ')',
249
497
  ';',
250
498
  ];
@@ -273,9 +521,13 @@ export function printAlterTable(node, opts) {
273
521
  const sourcePartition = propStr(node, 'sourcePartition');
274
522
  const targetTable = prop(node, 'targetTable');
275
523
  const targetPartition = propStr(node, 'targetPartition');
524
+ const switchOptions = node.props?.['switchOptions'];
276
525
  const sourceDoc = sourcePartition ? [' ', keyword('PARTITION', opts), ' ', sourcePartition] : '';
277
526
  const targetDoc = targetTable ? schemaObjectName(targetTable) : '';
278
527
  const targetPartDoc = targetPartition ? [' ', keyword('PARTITION', opts), ' ', targetPartition] : '';
528
+ const switchOptDoc = switchOptions?.length
529
+ ? [' ', keyword('WITH', opts), ' (', join(', ', switchOptions), ')']
530
+ : '';
279
531
  return [
280
532
  keyword('ALTER TABLE', opts),
281
533
  ' ',
@@ -288,9 +540,18 @@ export function printAlterTable(node, opts) {
288
540
  ' ',
289
541
  targetDoc,
290
542
  targetPartDoc,
543
+ switchOptDoc,
291
544
  ';',
292
545
  ];
293
546
  }
547
+ if (alterType === 'AlterTableTriggerModificationStatement') {
548
+ const enable = propBool(node, 'enable');
549
+ const triggerAll = node.props?.['triggerAll'];
550
+ const triggerNames = node.props?.['triggerNames'];
551
+ const verb = enable ? keyword('ENABLE TRIGGER', opts) : keyword('DISABLE TRIGGER', opts);
552
+ const targets = triggerAll ? keyword('ALL', opts) : join(', ', triggerNames ?? []);
553
+ return [keyword('ALTER TABLE', opts), ' ', name, hardline, verb, ' ', targets, ';'];
554
+ }
294
555
  return [keyword('ALTER TABLE', opts), ' ', name, ' /* ', alterType, ' */;'];
295
556
  }
296
557
  // ---------------------------------------------------------------------------
@@ -299,10 +560,10 @@ export function printAlterTable(node, opts) {
299
560
  export function printCreateIndex(node, opts) {
300
561
  const indexName = propStr(node, 'indexName') ?? 'idx';
301
562
  const isUnique = propBool(node, 'unique');
302
- const isClustered = propBool(node, 'clustered');
303
563
  const table = prop(node, 'table');
304
564
  const columns = propArr(node, 'columns');
305
565
  const includeColumns = node.props?.['includeColumns'];
566
+ const filterPredicate = propStr(node, 'filterPredicate');
306
567
  const colDocs = columns.map((c) => {
307
568
  const colName = propStr(c, 'name') ?? c.text ?? '';
308
569
  const sort = propStr(c, 'sortOrder') ?? 'Ascending';
@@ -310,8 +571,14 @@ export function printCreateIndex(node, opts) {
310
571
  ? [colName, ' ', keyword('DESC', opts)]
311
572
  : [colName, ' ', keyword('ASC', opts)];
312
573
  });
313
- const uniqueKw = isUnique ? keyword('UNIQUE ', opts) : '';
314
- const clusteredKw = isClustered ? keyword('CLUSTERED ', opts) : keyword('NONCLUSTERED ', opts);
574
+ const uniqueKw = isUnique ? [keyword('UNIQUE', opts), ' '] : '';
575
+ // Preserve CLUSTERED / NONCLUSTERED exactly as written; omit when not specified.
576
+ const clusteredProp = node.props?.['clustered'];
577
+ const clusteredKw = clusteredProp === true
578
+ ? [keyword('CLUSTERED', opts), ' ']
579
+ : clusteredProp === false
580
+ ? [keyword('NONCLUSTERED', opts), ' ']
581
+ : '';
315
582
  const onClause = [
316
583
  keyword('ON', opts),
317
584
  ' ',
@@ -322,21 +589,26 @@ export function printCreateIndex(node, opts) {
322
589
  ')',
323
590
  ];
324
591
  const includePart = Array.isArray(includeColumns) && includeColumns.length > 0
325
- ? [
326
- hardline,
327
- keyword('INCLUDE', opts),
328
- ' ',
329
- parenList(includeColumns),
330
- ]
592
+ ? [hardline, keyword('INCLUDE', opts), ' ', parenList(includeColumns)]
331
593
  : '';
594
+ const filterPart = filterPredicate ? [hardline, keyword('WHERE', opts), ' ', filterPredicate] : '';
595
+ const indexOptions = node.props?.['indexOptions'];
596
+ const withPart = indexOptions && indexOptions.length > 0
597
+ ? [hardline, keyword('WITH', opts), ' (', join(', ', indexOptions), ')']
598
+ : '';
599
+ const onFileGroup = propStr(node, 'onFileGroup');
600
+ const fileGroupPart = onFileGroup ? [hardline, keyword('ON', opts), ' ', onFileGroup] : '';
332
601
  return group([
333
- keyword('CREATE ', opts),
602
+ keyword('CREATE', opts),
603
+ ' ',
334
604
  uniqueKw,
335
605
  clusteredKw,
336
606
  keyword('INDEX', opts),
337
607
  ' ',
338
608
  indexName,
339
- indent([hardline, onClause, includePart]),
609
+ indent([hardline, onClause, includePart, filterPart]),
610
+ withPart,
611
+ fileGroupPart,
340
612
  ';',
341
613
  ]);
342
614
  }
@@ -354,6 +626,12 @@ export function printAlterIndex(node, opts) {
354
626
  Set: 'SET',
355
627
  };
356
628
  const typeKw = keyword(typeKwMap[alterType] ?? alterType.toUpperCase(), opts);
629
+ const indexOptions = node.props?.['indexOptions'];
630
+ const partition = propStr(node, 'partition');
631
+ const withPart = indexOptions && indexOptions.length > 0
632
+ ? [' ', keyword('WITH', opts), ' (', join(', ', indexOptions), ')']
633
+ : '';
634
+ const partitionPart = partition ? [' ', keyword('PARTITION', opts), ' = ', partition] : '';
357
635
  return [
358
636
  keyword('ALTER INDEX', opts),
359
637
  ' ',
@@ -364,6 +642,8 @@ export function printAlterIndex(node, opts) {
364
642
  schemaObjectName(table),
365
643
  ' ',
366
644
  typeKw,
645
+ partitionPart,
646
+ withPart,
367
647
  ';',
368
648
  ];
369
649
  }
@@ -410,10 +690,13 @@ export function printCreateProcedure(node, opts) {
410
690
  const paramDocs = parameters.map((p) => {
411
691
  const pName = propStr(p, 'name') ?? '@p';
412
692
  const dt = propStr(p, 'dataType') ?? 'INT';
693
+ const isUdt = propBool(p, 'isUdt');
413
694
  const isOutput = propBool(p, 'output');
414
695
  const isReadonly = propBool(p, 'readonly');
415
696
  const defaultVal = prop(p, 'defaultValue');
416
- const parts = [pName, ' ', keyword(dt, opts)];
697
+ // UDT names are identifiers, not SQL keywords — skip keyword-casing
698
+ const dtDoc = isUdt ? dt : keyword(dt, opts);
699
+ const parts = [pName, ' ', dtDoc];
417
700
  if (defaultVal)
418
701
  parts.push(' = ', printNode(defaultVal, opts));
419
702
  if (isOutput)
@@ -422,7 +705,11 @@ export function printCreateProcedure(node, opts) {
422
705
  parts.push(' ', keyword('READONLY', opts));
423
706
  return parts;
424
707
  });
425
- const bodyDocs = body.map((s) => printStatementWithComments(s, opts));
708
+ // Natively compiled procs have a single BEGIN ATOMIC WITH (...) body statement.
709
+ const atomicBlock = body.length === 1 && body[0].type === 'BeginEndAtomicBlock' ? body[0] : null;
710
+ const atomicOptions = atomicBlock?.props?.['atomicOptions'];
711
+ const innerBody = atomicBlock ? propArr(atomicBlock, 'statements') : unwrapBodyBlock(body);
712
+ const bodyDocs = innerBody.map((s) => printStatementWithComments(s, opts));
426
713
  const preBody = commentsBlock(node.preBodyComments);
427
714
  const postParam = commentsBlock(node.postParamComments);
428
715
  const procKw = node.type === 'CreateOrAlterProcedureStatement'
@@ -430,6 +717,26 @@ export function printCreateProcedure(node, opts) {
430
717
  : node.type === 'AlterProcedureStatement'
431
718
  ? keyword('ALTER PROCEDURE', opts)
432
719
  : keyword('CREATE PROCEDURE', opts);
720
+ // CLR stored procedure: AS EXTERNAL NAME assembly.[class].method
721
+ const externalName = propStr(node, 'externalName');
722
+ if (externalName) {
723
+ return group([
724
+ procKw,
725
+ ' ',
726
+ schemaObjectName(prop(node, 'name')),
727
+ preBody,
728
+ parameters.length > 0 ? indent([hardline, join([',', hardline], paramDocs)]) : '',
729
+ postParam,
730
+ printModuleOptions(node, opts),
731
+ hardline,
732
+ keyword('AS', opts),
733
+ ' ',
734
+ keyword('EXTERNAL NAME', opts),
735
+ ' ',
736
+ externalName,
737
+ ';',
738
+ ]);
739
+ }
433
740
  return group([
434
741
  procKw,
435
742
  ' ',
@@ -441,7 +748,19 @@ export function printCreateProcedure(node, opts) {
441
748
  hardline,
442
749
  keyword('AS', opts),
443
750
  hardline,
444
- keyword('BEGIN', opts),
751
+ ...(atomicOptions?.length
752
+ ? [
753
+ keyword('BEGIN', opts),
754
+ ' ',
755
+ keyword('ATOMIC', opts),
756
+ ' ',
757
+ keyword('WITH', opts),
758
+ ' (',
759
+ indent([hardline, join([',', hardline], atomicOptions)]),
760
+ hardline,
761
+ ')',
762
+ ]
763
+ : [keyword('BEGIN', opts)]),
445
764
  indent([hardline, join([hardline, hardline], bodyDocs)]),
446
765
  hardline,
447
766
  keyword('END', opts),
@@ -459,7 +778,9 @@ export function printCreateFunction(node, opts) {
459
778
  const paramDocs = parameters.map((p) => {
460
779
  const pName = propStr(p, 'name') ?? '@p';
461
780
  const dt = propStr(p, 'dataType') ?? 'INT';
462
- return [pName, ' ', keyword(dt, opts)];
781
+ const isUdt = propBool(p, 'isUdt');
782
+ // UDT names are identifiers — skip keyword-casing
783
+ return [pName, ' ', isUdt ? dt : keyword(dt, opts)];
463
784
  });
464
785
  const preBody = commentsBlock(node.preBodyComments);
465
786
  const postParam = commentsBlock(node.postParamComments);
@@ -468,25 +789,43 @@ export function printCreateFunction(node, opts) {
468
789
  : node.type === 'AlterFunctionStatement'
469
790
  ? keyword('ALTER FUNCTION', opts)
470
791
  : keyword('CREATE FUNCTION', opts);
471
- const nameAndParams = [
792
+ const nameAndParamsNoOpts = [
472
793
  fnKw,
473
794
  ' ',
474
795
  schemaObjectName(prop(node, 'name')),
475
796
  preBody,
476
797
  group(['(', parameters.length > 0 ? [indent([softline, join([',', line], paramDocs)]), softline] : '', ')']),
477
798
  postParam,
478
- printModuleOptions(node, opts),
479
799
  ];
800
+ // CLR function: EXTERNAL NAME assembly.[class].method (no body)
801
+ const externalName = propStr(node, 'externalName');
802
+ if (externalName) {
803
+ return [
804
+ nameAndParamsNoOpts,
805
+ hardline,
806
+ keyword('RETURNS', opts),
807
+ ' ',
808
+ keyword(returnType, opts),
809
+ printModuleOptions(node, opts),
810
+ hardline,
811
+ keyword('AS', opts),
812
+ ' ',
813
+ keyword('EXTERNAL NAME', opts),
814
+ ' ',
815
+ externalName,
816
+ ';',
817
+ ];
818
+ }
480
819
  if (bodyType === 'table') {
481
- // Inline TVF: RETURNS TABLE AS RETURN (query) — no BEGIN/END
482
- // returnType raw text contains the SELECT, not the word TABLE — hardcode TABLE
820
+ // Inline TVF: RETURNS TABLE [WITH options] AS RETURN (query) — no BEGIN/END
483
821
  const queryDoc = body && !Array.isArray(body) ? qexpr(body, opts) : '/* query */';
484
822
  return [
485
- nameAndParams,
486
- ' ',
823
+ nameAndParamsNoOpts,
824
+ hardline,
487
825
  keyword('RETURNS', opts),
488
826
  ' ',
489
827
  keyword('TABLE', opts),
828
+ printModuleOptions(node, opts),
490
829
  hardline,
491
830
  keyword('AS', opts),
492
831
  hardline,
@@ -499,7 +838,7 @@ export function printCreateFunction(node, opts) {
499
838
  ];
500
839
  }
501
840
  // Scalar or multi-statement TVF — both use BEGIN...END
502
- const stmts = Array.isArray(body) ? body.map((s) => printStatementWithComments(s, opts)) : [];
841
+ const stmts = Array.isArray(body) ? unwrapBodyBlock(body).map((s) => printStatementWithComments(s, opts)) : [];
503
842
  const bodyDoc = join([hardline, hardline], stmts);
504
843
  let retTypePart;
505
844
  if (bodyType === 'inline-table') {
@@ -519,12 +858,15 @@ export function printCreateFunction(node, opts) {
519
858
  else {
520
859
  retTypePart = keyword(returnType, opts);
521
860
  }
861
+ // WITH options come AFTER RETURNS (per T-SQL syntax):
862
+ // CREATE FUNCTION ... (params) RETURNS type WITH options AS BEGIN ... END
522
863
  return [
523
- nameAndParams,
524
- ' ',
864
+ nameAndParamsNoOpts,
865
+ hardline,
525
866
  keyword('RETURNS', opts),
526
867
  ' ',
527
868
  retTypePart,
869
+ printModuleOptions(node, opts),
528
870
  hardline,
529
871
  keyword('AS', opts),
530
872
  hardline,
@@ -547,9 +889,7 @@ export function printCreateView(node, opts) {
547
889
  : node.type === 'AlterViewStatement'
548
890
  ? keyword('ALTER VIEW', opts)
549
891
  : keyword('CREATE VIEW', opts);
550
- const colsPart = columns?.length
551
- ? [' ', parenList(columns)]
552
- : '';
892
+ const colsPart = columns?.length ? [' ', parenList(columns)] : '';
553
893
  const withPart = withOptions?.length
554
894
  ? [
555
895
  hardline,
@@ -559,6 +899,8 @@ export function printCreateView(node, opts) {
559
899
  ]
560
900
  : '';
561
901
  const preBodyPart = commentsBlock(node.preBodyComments);
902
+ const withCheckOption = node.props?.['withCheckOption'];
903
+ const checkOptionPart = withCheckOption ? [hardline, keyword('WITH CHECK OPTION', opts)] : '';
562
904
  return group([
563
905
  kw,
564
906
  ' ',
@@ -570,6 +912,7 @@ export function printCreateView(node, opts) {
570
912
  keyword('AS', opts),
571
913
  hardline,
572
914
  body ? qexpr(body, opts) : '',
915
+ checkOptionPart,
573
916
  ';',
574
917
  ]);
575
918
  }
@@ -589,7 +932,15 @@ export function printCreateTrigger(node, opts) {
589
932
  const actionList = Array.isArray(actions)
590
933
  ? join(', ', actions.map((a) => keyword(a.toUpperCase(), opts)))
591
934
  : '';
592
- const bodyDocs = propArr(node, 'body').map((s) => printStatementWithComments(s, opts));
935
+ const notForReplication = propBool(node, 'notForReplication');
936
+ const notForReplicationDoc = notForReplication ? [hardline, keyword('NOT FOR REPLICATION', opts)] : '';
937
+ const bodyDocs = unwrapBodyBlock(propArr(node, 'body')).map((s) => printStatementWithComments(s, opts));
938
+ const triggerScope = propStr(node, 'triggerScope'); // 'Database' or 'Server' for DDL triggers
939
+ const onTarget = triggerScope === 'Database'
940
+ ? keyword('DATABASE', opts)
941
+ : triggerScope === 'Server'
942
+ ? keyword('ALL SERVER', opts)
943
+ : schemaObjectName(prop(node, 'onName'));
593
944
  return [
594
945
  kw,
595
946
  ' ',
@@ -597,11 +948,12 @@ export function printCreateTrigger(node, opts) {
597
948
  hardline,
598
949
  keyword('ON', opts),
599
950
  ' ',
600
- schemaObjectName(prop(node, 'onName')),
951
+ onTarget,
601
952
  hardline,
602
953
  typeKw,
603
954
  ' ',
604
955
  actionList,
956
+ notForReplicationDoc,
605
957
  hardline,
606
958
  keyword('AS', opts),
607
959
  hardline,
@@ -749,9 +1101,14 @@ export function printDropObjects(objType, node, opts) {
749
1101
  ];
750
1102
  }
751
1103
  export function printDropIndex(node, opts) {
1104
+ const ifExists = propBool(node, 'ifExists');
752
1105
  const indices = propArr(node, 'indices');
753
1106
  const indexDocs = indices.map((idx) => [propStr(idx, 'name') ?? '', ' ', keyword('ON', opts), ' ', schemaObjectName(prop(idx, 'table'))]);
754
- return group([keyword('DROP INDEX', opts), ' ', join([',', hardline], indexDocs), ';']);
1107
+ const ifExistsPart = ifExists ? [' ', keyword('IF EXISTS', opts)] : '';
1108
+ if (indexDocs.length === 1) {
1109
+ return [keyword('DROP INDEX', opts), ifExistsPart, ' ', indexDocs[0], ';'];
1110
+ }
1111
+ return [keyword('DROP INDEX', opts), ifExistsPart, indent([hardline, join([',', hardline], indexDocs)]), ';'];
755
1112
  }
756
1113
  // ---------------------------------------------------------------------------
757
1114
  // CREATE / DROP SYNONYM
@@ -964,7 +1321,10 @@ export function printUpdateStatistics(node, opts) {
964
1321
  const subElements = node.props?.['subElements'];
965
1322
  const options = node.props?.['options'];
966
1323
  const parts = [keyword('UPDATE STATISTICS', opts), ' ', table ? schemaObjectName(table) : ''];
967
- if (subElements?.length)
1324
+ // Single stat name: no parens needed. Multiple: wrap in parens.
1325
+ if (subElements?.length === 1)
1326
+ parts.push([' ', subElements[0]]);
1327
+ else if (subElements?.length)
968
1328
  parts.push([' ', parenList(subElements)]);
969
1329
  if (options?.length)
970
1330
  parts.push([hardline, withOptionsClause(options, opts)]);