Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/guide/formatting-recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,38 @@ Anonymous style prints bare symbols such as `?` or `%s`. `SqlFormatter` still re
## Learn More

Check the full [`SqlFormatterOptions` API](../api/interfaces/SqlFormatterOptions.md) documentation for every toggle, including advanced preset configuration and default values.

## Formatting DDL statements

`SqlFormatter` now understands schema-definition statements. You can parse `CREATE TABLE`, `DROP TABLE`, `ALTER TABLE` constraint changes, and index management statements and feed the resulting ASTs through the formatter to keep them consistent with query output.

```ts
import {
CreateTableParser,
DropTableParser,
CreateIndexParser,
DropIndexParser,
DropConstraintParser,
AlterTableParser,
SqlFormatter
} from 'rawsql-ts';

const ddl = `CREATE TABLE IF NOT EXISTS public.users (
id BIGINT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
role_id INT REFERENCES auth.roles(id)
) WITH (fillfactor = 80)`;

const ast = CreateTableParser.parse(ddl);
const { formattedSql } = new SqlFormatter({ keywordCase: 'lower' }).format(ast);
// formattedSql => drop-in-ready canonical SQL
```

Use the dedicated parsers when working with other DDL statements:

- `DropTableParser` for `DROP TABLE` with multi-table targets and cascading options.
- `AlterTableParser` to capture `ADD CONSTRAINT`/`DROP CONSTRAINT` actions on existing tables.
- `CreateIndexParser` and `DropIndexParser` to normalize index definitions, including INCLUDE lists, storage parameters, and partial index predicates.
- `DropConstraintParser` when databases support standalone constraint removal.

These parsers emit strongly typed models (`CreateTableQuery`, `CreateIndexStatement`, `AlterTableStatement`, and more) so the formatter and other visitors can treat DDL alongside queries.
6 changes: 6 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export * from './parsers/DeleteQueryParser';
export * from './parsers/WithClauseParser';
export * from './parsers/CreateTableParser';
export * from './parsers/MergeQueryParser';
export * from './parsers/CreateIndexParser';
export * from './parsers/DropTableParser';
export * from './parsers/DropIndexParser';
export * from './parsers/AlterTableParser';
export * from './parsers/DropConstraintParser';

export * from './models/BinarySelectQuery';
export * from './models/SelectQuery';
Expand All @@ -22,6 +27,7 @@ export * from './models/UpdateQuery';
export * from './models/DeleteQuery';
export * from './models/CreateTableQuery';
export * from './models/MergeQuery';
export * from './models/DDLStatements';

export * from './transformers/CTECollector';
export * from './transformers/CTENormalizer';
Expand Down
198 changes: 184 additions & 14 deletions packages/core/src/models/CreateTableQuery.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,187 @@
import { SqlComponent } from "./SqlComponent";
import type { SelectQuery } from "./SelectQuery";
import { ColumnReference, FunctionCall, IdentifierString, RawString } from "./ValueComponent";
import {
ColumnReference,
FunctionCall,
IdentifierString,
RawString,
ValueComponent,
TypeValue,
QualifiedName
} from "./ValueComponent";
import { SimpleSelectQuery } from "./SimpleSelectQuery";
import { SelectClause, SelectItem, FromClause, TableSource, SourceExpression } from "./Clause";
import { SelectValueCollector } from "../transformers/SelectValueCollector";

// Represents a CREATE TABLE query model
// Supports temporary tables and AS SELECT ...
export type ReferentialAction = 'cascade' | 'restrict' | 'no action' | 'set null' | 'set default';
export type ConstraintDeferrability = 'deferrable' | 'not deferrable' | null;
export type ConstraintInitially = 'immediate' | 'deferred' | null;
export type MatchType = 'full' | 'partial' | 'simple' | null;

/**
* Represents a REFERENCES clause definition that can be shared between column and table constraints.
*/
export class ReferenceDefinition extends SqlComponent {
static kind = Symbol("ReferenceDefinition");
targetTable: QualifiedName;
columns: IdentifierString[] | null;
matchType: MatchType;
onDelete: ReferentialAction | null;
onUpdate: ReferentialAction | null;
deferrable: ConstraintDeferrability;
initially: ConstraintInitially;

constructor(params: {
targetTable: QualifiedName;
columns?: IdentifierString[] | null;
matchType?: MatchType;
onDelete?: ReferentialAction | null;
onUpdate?: ReferentialAction | null;
deferrable?: ConstraintDeferrability;
initially?: ConstraintInitially;
}) {
super();
this.targetTable = params.targetTable;
this.columns = params.columns ? [...params.columns] : null;
this.matchType = params.matchType ?? null;
this.onDelete = params.onDelete ?? null;
this.onUpdate = params.onUpdate ?? null;
this.deferrable = params.deferrable ?? null;
this.initially = params.initially ?? null;
}
}

export type ColumnConstraintKind =
| 'not-null'
| 'null'
| 'default'
| 'primary-key'
| 'unique'
| 'references'
| 'check'
| 'generated-always-identity'
| 'generated-by-default-identity'
| 'raw';

/**
* Column-level constraint definition.
*/
export class ColumnConstraintDefinition extends SqlComponent {
static kind = Symbol("ColumnConstraintDefinition");
kind: ColumnConstraintKind;
constraintName?: IdentifierString;
defaultValue?: ValueComponent;
checkExpression?: ValueComponent;
reference?: ReferenceDefinition;
rawClause?: RawString;

constructor(params: {
kind: ColumnConstraintKind;
constraintName?: IdentifierString;
defaultValue?: ValueComponent;
checkExpression?: ValueComponent;
reference?: ReferenceDefinition;
rawClause?: RawString;
}) {
super();
this.kind = params.kind;
this.constraintName = params.constraintName;
this.defaultValue = params.defaultValue;
this.checkExpression = params.checkExpression;
this.reference = params.reference;
this.rawClause = params.rawClause;
}
}

export type TableConstraintKind = 'primary-key' | 'unique' | 'foreign-key' | 'check' | 'raw';

/**
* Table-level constraint definition.
*/
export class TableConstraintDefinition extends SqlComponent {
static kind = Symbol("TableConstraintDefinition");
kind: TableConstraintKind;
constraintName?: IdentifierString;
columns: IdentifierString[] | null;
reference?: ReferenceDefinition;
checkExpression?: ValueComponent;
rawClause?: RawString;
deferrable: ConstraintDeferrability;
initially: ConstraintInitially;

constructor(params: {
kind: TableConstraintKind;
constraintName?: IdentifierString;
columns?: IdentifierString[] | null;
reference?: ReferenceDefinition;
checkExpression?: ValueComponent;
rawClause?: RawString;
deferrable?: ConstraintDeferrability;
initially?: ConstraintInitially;
}) {
super();
this.kind = params.kind;
this.constraintName = params.constraintName;
this.columns = params.columns ? [...params.columns] : null;
this.reference = params.reference;
this.checkExpression = params.checkExpression;
this.rawClause = params.rawClause;
this.deferrable = params.deferrable ?? null;
this.initially = params.initially ?? null;
}
}

/**
* Represents a single column definition within CREATE TABLE.
*/
export class TableColumnDefinition extends SqlComponent {
static kind = Symbol("TableColumnDefinition");
name: IdentifierString;
dataType?: TypeValue | RawString;
constraints: ColumnConstraintDefinition[];

constructor(params: {
name: IdentifierString;
dataType?: TypeValue | RawString;
constraints?: ColumnConstraintDefinition[];
}) {
super();
this.name = params.name;
this.dataType = params.dataType;
this.constraints = params.constraints ? [...params.constraints] : [];
}
}

// Represents a CREATE TABLE query model that supports column definitions and AS SELECT variants.
export class CreateTableQuery extends SqlComponent {
/** SqlComponent kind symbol for visitor pattern */
static kind = Symbol("CreateTableQuery");
/** Table name (with optional schema) */
tableName: IdentifierString;
/** If true, this is a temporary table */
namespaces: string[] | null;
isTemporary: boolean;
/** If true, the statement includes IF NOT EXISTS */
ifNotExists: boolean;
/** Optional: SELECT query for AS SELECT ... */
columns: TableColumnDefinition[];
tableConstraints: TableConstraintDefinition[];
tableOptions?: RawString | null;
asSelectQuery?: SelectQuery;

constructor(params: {
tableName: string;
namespaces?: string[] | null;
isTemporary?: boolean;
ifNotExists?: boolean;
columns?: TableColumnDefinition[];
tableConstraints?: TableConstraintDefinition[];
tableOptions?: RawString | null;
asSelectQuery?: SelectQuery;
}) {
super();
this.tableName = new IdentifierString(params.tableName);
this.namespaces = params.namespaces ? [...params.namespaces] : null;
this.isTemporary = params.isTemporary ?? false;
this.ifNotExists = params.ifNotExists ?? false;
this.columns = params.columns ? [...params.columns] : [];
this.tableConstraints = params.tableConstraints ? [...params.tableConstraints] : [];
this.tableOptions = params.tableOptions ?? null;
this.asSelectQuery = params.asSelectQuery;
}

Expand All @@ -37,23 +190,36 @@ export class CreateTableQuery extends SqlComponent {
*/
getSelectQuery(): SimpleSelectQuery {
let selectItems: SelectItem[];

// Prefer explicit AS SELECT query columns when present.
if (this.asSelectQuery) {
// Use SelectValueCollector to get columns from asSelectQuery
const collector = new SelectValueCollector();
const values = collector.collect(this.asSelectQuery);
selectItems = values.map(val => new SelectItem(val.value, val.name));
} else if (this.columns.length > 0) {
// Use defined column names when the table definition is DDL-based.
selectItems = this.columns.map(column => new SelectItem(
new ColumnReference(null, column.name),
column.name.name
));
} else {
// fallback: wildcard
// Fallback to wild-card selection when no column metadata is available.
selectItems = [new SelectItem(new RawString("*"))];
}

// Build a simple SELECT ... FROM table query.
const qualifiedName = this.namespaces && this.namespaces.length > 0
? [...this.namespaces, this.tableName.name].join(".")
: this.tableName.name;

Comment on lines +210 to +214
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Schema qualification bug: passing dotted string as table name breaks quoting.

You build "schema.table" as a single IdentifierString with namespaces=null, yielding wrong output like "schema.table". Pass namespaces and table separately to TableSource.

-        // Build a simple SELECT ... FROM table query.
-        const qualifiedName = this.namespaces && this.namespaces.length > 0
-            ? [...this.namespaces, this.tableName.name].join(".")
-            : this.tableName.name;
+        // Build a simple SELECT ... FROM table query.
+        const tableSource = new TableSource(this.namespaces ?? null, this.tableName);
@@
-                    new TableSource(null, qualifiedName),
+                    tableSource,

Also applies to: 219-223

🤖 Prompt for AI Agents
In packages/core/src/models/CreateTableQuery.ts around lines 210-214 (and
similarly 219-223), the code builds a dotted "schema.table" string and sets
namespaces=null which breaks identifier quoting; instead construct the
TableSource (or relevant AST node) with namespaces as an array and tableName as
a bare IdentifierString (no dots) so the renderer can quote each part
correctly—remove the concatenated qualifiedName and pass this.namespaces (if
any) and this.tableName.name separately when creating the TableSource/Identifier
node.

return new SimpleSelectQuery({
selectClause: new SelectClause(selectItems),
fromClause: new FromClause(
new SourceExpression(
new TableSource(null, this.tableName.name),
new TableSource(null, qualifiedName),
null
),
null // joins
null
),
});
}
Expand All @@ -62,16 +228,20 @@ export class CreateTableQuery extends SqlComponent {
* Returns a SelectQuery that counts all rows in this table.
*/
getCountQuery(): SimpleSelectQuery {
const qualifiedName = this.namespaces && this.namespaces.length > 0
? [...this.namespaces, this.tableName.name].join(".")
: this.tableName.name;

Comment on lines +231 to +234
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Same issue in getCountQuery: use namespaces-aware TableSource.

Ensure proper qualification and quoting.

-        const qualifiedName = this.namespaces && this.namespaces.length > 0
-            ? [...this.namespaces, this.tableName.name].join(".")
-            : this.tableName.name;
+        const tableSource = new TableSource(this.namespaces ?? null, this.tableName);
@@
-                    new TableSource(null, qualifiedName),
+                    tableSource,

Also applies to: 241-245

🤖 Prompt for AI Agents
packages/core/src/models/CreateTableQuery.ts lines 231-234 and 241-245: the code
builds a qualifiedName string manually and thus ignores the project's
TableSource logic for namespace-aware qualification and proper identifier
quoting; replace the manual qualifiedName construction with the project’s
TableSource helper (or factory) that accepts the TableName and namespaces so it
emits a correctly-qualified, quoted table reference (use the same TableSource
usage pattern as other queries in the repo), and update both locations to render
the TableSource instead of joining namespaces by hand.

return new SimpleSelectQuery({
selectClause: new SelectClause([
new SelectItem(new FunctionCall(null, "count", new ColumnReference(null, "*"), null))
]),
fromClause: new FromClause(
new SourceExpression(
new TableSource(null, this.tableName.name),
new TableSource(null, qualifiedName),
null
),
null // joins
null
),
});
}
Expand Down
Loading
Loading