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
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ jobs:
& ./scripts/install-scriptdom.ps1
$assembly = (Resolve-Path ./scripts/lib/Microsoft.SqlServer.TransactSql.ScriptDom.dll).Path
& ./tests/formatter.ps1 -AssemblyPath $assembly
& ./tests/feedback.ps1 -AssemblyPath $assembly
& ./tests/cli.ps1 -AssemblyPath $assembly
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# roy-sql

My SQL house style, as an agent skill and a local T-SQL formatter.
My SQL house style, as an agent skill and a local SQL-like formatter with T-SQL awareness.
Lowercase keywords, leading commas, alias-first projections, expanded joins and CTEs.
Plain SQL, comments that explain why, no query redesign disguised as formatting.

Expand All @@ -24,19 +24,23 @@ From the repository directory:
```powershell
pwsh -NoProfile -File scripts/install-scriptdom.ps1
pwsh -NoProfile -File scripts/format-sql.ps1 -Path query.sql
pwsh -NoProfile -File scripts/format-sql.ps1 -Path query.sql -Strict
pwsh -NoProfile -File scripts/format-sql.ps1 -Path query.sql -Check
```

The first command downloads a hash-pinned Microsoft ScriptDOM dependency. The formatter
then runs locally, with no model, database, network call or input-file edit. Its implementation
is visible PowerShell and C# source; no separate .NET SDK is needed.

SQL goes to stdout. Diagnostics go to stderr. Exit 0 means success, 1 means check-mode
drift, 2 means refusal or setup failure. Save stdout to a different file, never the input path.
SQL goes to stdout. Diagnostics go to stderr. Omit `-Path` to pipe SQL on stdin.
Exit 0 means output produced, 1 means check-mode drift, 2 means strict refusal or an
operational failure. Save stdout to a different file, never the input path.

The mechanical helper covers **T-SQL SELECTs**, not all of the skill's authoring guidance.
Unsupported syntax, including USE, DML, DDL and UNION, refuses with no partial SQL.
Preservation checks are not proof that a query returns the same results on a database.
Recognized **T-SQL SELECTs** get structural formatting. Unfamiliar syntax, DDL and incomplete
SQL get conservative token layout with a warning, or unchanged input if tokenization is unsafe.
`-Strict` retains the original SELECT-focused refusal behavior and emits no partial SQL.
Neither mode executes SQL or proves database validity. There is no dialect translation:
LIMIT stays LIMIT. Missing files/dependencies and write failures still report errors.
See [coverage, checks and limitations](references/formatter.md).

## License
Expand Down
73 changes: 46 additions & 27 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: roy-sql
description: "Write or reformat SQL in the roy-sql house style: lowercase keywords, leading commas, alias-first projections, expanded joins and CTEs, and comments that explain why. Use for roy-sql, match my SQL style, or format this query in this profile. Includes an optional local T-SQL formatter. Preserve existing names and values when reformatting; this is not a database execution or query optimization tool."
license: MIT
metadata:
version: 2.27.0
version: 2.28.0
publishable: true
---

Expand All @@ -15,9 +15,9 @@ When they conflict, the principle wins. Read principles before mechanics.
New to the terms? [roy-sql, in plain language](references/style-explained.md) explains each
piece with a small example, and separates formatting from query-design choices.

For deterministic expanded T-SQL layout, the [formatter guide](references/formatter.md)
documents the local helper, its admitted syntax and safe refusals.
The compact authoring latitude below is not part of its v1 output.
For expanded T-SQL layout with best-effort handling of unfamiliar SQL-like text, the
[formatter guide](references/formatter.md) documents the local helper and optional strict mode.
The compact authoring latitude below is not part of its automatic output.

## Two modes: authoring vs reformatting

Expand Down Expand Up @@ -212,7 +212,26 @@ declare @orderid int = try_cast(
Default to `left(name, 2)`: no space before `(` or just inside either parenthesis.
Hand-formatted SQL may use `left( name, 2 )`, or tier long nested calls across lines with
their closing parentheses on separate lines. This is readability latitude, not a fixed-width
rule. The mechanical v1 formatter uses the tight default; automatic nested-call wrapping is deferred.
rule. The mechanical formatter uses the tight default; automatic nested-call wrapping is deferred.

## CASE blocks and TOP

Treat `case` / `end` as a block: put each `when` and `else` one indent unit inside,
and close with `end` at the expression's base indentation. Nested CASEs nest another unit.
Very short hand-written CASEs may stay inline. No universal length cutoff is established;
the mechanical formatter expands CASEs rather than guessing that exception.

```sql
[tier] = case
when amount > 100 then 'high'
when amount > 50 then 'medium'
else 'low'
end
```

Use `select top 50`, not `select top (50)`, for an integer row count. Parentheses around
variable/calculated TOP expressions are retained by the formatter. Do not translate another
dialect's LIMIT to TOP: that is not formatting.

## Column lists

Expand Down Expand Up @@ -254,19 +273,20 @@ Apply these to tables, subqueries/apply blocks and CTEs:

## FROM and joins

`from` stands alone. The first table is one unit in, join targets another unit in,
and each ON condition another unit in. `on` ENDS the join line. Put the new table on
`from` stands alone. The first table and every JOIN/APPLY are one unit in, aligned with
each other. ON conditions are one further unit in. Write `join`, not `inner join`;
LEFT/RIGHT/FULL/CROSS qualifiers retain their meaning. `on` ENDS the join line. Put the new table on
the LEFT of `=` and established/parent table on the RIGHT (`od.orderid = o.orderid`).
Order joins down the domain hierarchy: orders → details → products → categories.

```sql
from
orders o
join [order details] od on
od.orderid = o.orderid
join products p on
p.productid = od.productid
and p.discontinued = 0
join [order details] od on
od.orderid = o.orderid
join products p on
p.productid = od.productid
and p.discontinued = 0
where
o.orderid = @orderid
and o.shippeddate is not null
Expand Down Expand Up @@ -309,29 +329,28 @@ collapse expanded SQL just because it fits. When uncertain, expand.
where
shippeddate is not null

)

-- orders containing at least one beverages line
, withbev as (
), withbev as (

-- orders containing at least one beverages line
select
gd.orderid
from
gd
join [order details] od on
od.orderid = gd.orderid
join products p on
p.productid = od.productid
join categories c on
c.categoryid = p.categoryid
and c.categoryname = 'beverages'
join [order details] od on
od.orderid = gd.orderid
join products p on
p.productid = od.productid
join categories c on
c.categoryid = p.categoryid
and c.categoryname = 'beverages'

)
```

Use a new-line `;with` to defend against a prior batch without `go`. Keep one blank
line after each opening `(` and before its closing `)`. Subsequent CTEs have a leading
comma and a short `--` comment above naming what they find/exclude.
line after each opening `(` and before its closing `)`. Chain the next CTE on the closing
line: `), next as (`. No blank line between CTEs. A short purpose comment can go inside
the next CTE's body; preserve existing comments when reformatting.
CTE names are pure lowercase without underscores by default, snake_case when ambiguity
or meaningful prefixes warrant it (`ord_rev`, `ord_pay`), PascalCase last resort.
The scoping CTE is conventionally `gd` (good/got data).
Expand Down Expand Up @@ -390,8 +409,8 @@ Projection brackets and alias rules apply inside subqueries too.
## Indentation

One **tab-over unit per construct**, not a single indent per file. Clause keywords sit
at their block level, contents one unit in, joins one further unit from FROM's contents,
ON conditions one further unit, and CTE bodies one unit from their opening line.
at their block level, contents one unit in, joins aligned with FROM's contents,
ON conditions one further unit in, and CTE bodies one unit from their opening line.
Use the same width throughout a query. Mechanical output is **4 spaces, no tab bytes**.
I type tabs converted by my editor, usually to 4 spaces; where tabs can't be typed,
2 spaces is the minimum visible unit. The rule wins over a conflicting example.
Expand Down
98 changes: 98 additions & 0 deletions references/formatter-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,101 @@ select
from
[order details]
```

## CASE is a block

Before:

```sql
SELECT CASE WHEN amount > 100 THEN 'High' WHEN amount > 50 THEN 'Medium' ELSE 'Low' END AS tier FROM totals
```

After:

```sql
select
[tier] = case
when amount > 100 then 'High'
when amount > 50 then 'Medium'
else 'Low'
end
from
totals
```

## JOIN alignment and literal TOP

Before:

```sql
SELECT TOP (50) o.id FROM orders o INNER JOIN items i ON i.orderid=o.id LEFT JOIN products p ON p.id=i.productid
```

After:

```sql
select top 50
o.id
from
orders o
join items i on
i.orderid = o.id
left join products p on
p.id = i.productid
```

## Adjacent CTEs

Before:

```sql
WITH a AS (SELECT id FROM orders), b AS (SELECT id FROM a) SELECT id FROM b
```

After:

```sql
;with a as (

select
id
from
orders

), b as (

select
id
from
a

)

select
id
from
b
```

## LIMIT is retained, not translated

Before:

```sql
SELECT id FROM orders ORDER BY id LIMIT 50;
```

After:

```sql
select
id
from
orders
order by
id
limit
50;
```

Best-effort output. A diagnostic is written to stderr, not into the SQL.
49 changes: 39 additions & 10 deletions references/formatter.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Mechanical formatter

The helper formats existing **T-SQL SELECTs** into the expanded roy-sql layout. It makes no
model call, connects to no database and never writes the input file. It is not a
whole-language formatter.
The helper gives recognized **T-SQL SELECTs** expanded roy-sql layout, and makes a
best-effort layout pass over other SQL-like text. It makes no model call, connects to no
database and never writes the input file. It is not a whole-language parser or translator.

## Run

Expand All @@ -12,6 +12,7 @@ Install the dependency once (an explicit download from Microsoft's NuGet package
```powershell
pwsh -NoProfile -File scripts/install-scriptdom.ps1
pwsh -NoProfile -File scripts/format-sql.ps1 -Path query.sql
pwsh -NoProfile -File scripts/format-sql.ps1 -Path query.sql -Strict
```

Or supply an existing pinned assembly explicitly:
Expand All @@ -23,6 +24,10 @@ pwsh -NoProfile -File scripts/format-sql.ps1 -AssemblyPath /path/to/ScriptDom.dl

Omit `-Path` to read stdin. SQL goes to stdout, diagnostics to stderr. Exit **0** is successful
formatting or an already-formatted check, **1** is check-mode drift, **2** is refusal/setup failure.
Default mode produces output even when the syntax cannot be parsed: a token-layout fallback
reports its reason on stderr. If tokenization cannot retain the text, input is returned
unchanged with a warning. Missing/unreadable files, missing or mismatched dependencies,
compilation and output errors remain failures. `-Strict` opts into the original refusals.
Check mode emits no replacement SQL. Save output to a **different** path if wanted; shell
redirection to the input path can truncate it before this script runs.

Expand All @@ -37,20 +42,43 @@ tested by CI; check its actual result before calling a particular build cross-pl
## What it does

- Standalone clauses, leading projection commas, local alias/declaration alignment and
four-space construct-relative indentation. CTE interior blank lines, expanded nested queries.
four-space construct-relative indentation. JOIN/APPLY align with the primary table,
with conditions one level deeper. CTE interior blank lines, adjacent `), next as (` chains.
- Expanded CASE blocks. Short-case compaction remains an authoring choice, not an invented
character cutoff. Plain `join` replaces `inner join`; integer `top (50)` becomes `top 50`.
Parenthesized variable/calculated TOP expressions remain unchanged.
- Converts a named projection to `[Alias] = expression`, retaining the alias's exact value/case.
Adds optional `as` in variable declarations. These are syntax forms, not query-design edits.
- Lowercases lexer keywords, native types and recognized unqualified built-ins. Preserves
identifiers, variables, literals, date masks, comments and schema-qualified function names.
Common unqualified DATE_TRUNC/ARRAY_AGG are recognized for casing, not execution validity.
In default mode, bare TRUE/FALSE comparison operands are treated as SQL-like boolean words
and lowercased. `-Strict` preserves them as T-SQL identifiers. Quoted/qualified names stay intact.
- Preserves LF/CRLF choice. The canonical output ends with a newline.
- Reparses output, compares a canonical ScriptDOM-generated representation, and checks exact
comment content/order. The generator is used for comparison, not for printing source.

No `isnull` replacement, comparison/BETWEEN rewrite, guard insertion, name change, predicate movement, join reordering or
No `isnull` replacement, comparison/BETWEEN rewrite, guard insertion, predicate movement, join reordering or
compact-layout heuristic. Canonical comparison is a regression guard, **not proof of database
execution equivalence**. No SQL executes during formatting or the test suite.

## Coverage and refusals
## Best effort versus strict

Default mode first tries structural formatting. When that is unavailable, its fallback
organizes recognizable clause boundaries, projection/definition lists and parenthesized
query blocks. A multi-column SELECT pads its first item to align with the names after
leading commas; single-column lists do not. Nested SELECTs count independently.
Unknown words and operators retain their order. Inline spacing is conservative
because a T-SQL token boundary may split another dialect's operator. Comments and quoted
text are preserved; unclosed literals can cause the entire input to remain unchanged.
This is not full support for PostgreSQL, MySQL or arbitrary pseudocode, and layout may be
less polished than the T-SQL path. Warnings are deliberately outside the SQL output.

`-Strict` requires the admitted structural slice below and retains canonical-representation
and comment checks. It is a grammar/coverage check, not a schema, type or execution validator.
For example, a function name can parse without identifying a real SQL Server function.

## Strict coverage and refusals

The admitted slice is SELECT, normal named/function/derived table sources, joins/APPLY,
CTEs, TOP, WHERE/GROUP BY/HAVING/ORDER BY, CASE/window expressions, scalar/EXISTS/IN subqueries,
Expand All @@ -64,19 +92,20 @@ separator. Comments before a leading comma remain supported. String-valued proje
also refuse. Existing table/join hints and ordinary OVER expressions are preserved, not inserted.
Other unsupported parser shapes may refuse. SQLCMD commands/repeat counts are outside this slice.

A refusal emits **no partial SQL**. Unsupported valid SQL is a coverage gap, not a claim
that the original query is wrong. Read the diagnostic and format it with the skill's authoring
guidance, or add a reviewed fixture before expanding the helper's scope.
A strict refusal emits **no partial SQL**. Unsupported valid SQL is a coverage gap, not a claim
that the original query is wrong. Parser diagnostics include line, column and the parser's
message. Omit `-Strict` for best-effort output instead.

## Test

```powershell
pwsh -NoProfile -File tests/formatter.ps1 -AssemblyPath /path/to/ScriptDom.dll
pwsh -NoProfile -File tests/feedback.ps1 -AssemblyPath /path/to/ScriptDom.dll
pwsh -NoProfile -File tests/cli.ps1 -AssemblyPath /path/to/ScriptDom.dll
```

Exact layout fixtures, idempotence and safe-refusal checks use synthetic SQL. CI installs
the pinned dependency and runs both suites cold on Windows, macOS and Linux.
the pinned dependency and runs all three suites cold on Windows, macOS and Linux.
`-Check` checks for changes this formatter would make, not every prose rule in the skill.
A stable formatting bug can pass `-Check`; expected-output regression tests catch those mistakes.

Expand Down
4 changes: 2 additions & 2 deletions references/style-explained.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ the [worked examples](worked-examples.md) isolate each point in a short SQL exce
| selective brackets | use SQL Server's square-bracket quoting where a name needs it, and for renamed/computed output labels in this style | `[Customer Name]` or `[total]`, but a plain reference stays `o.orderid` |
| standalone clause keywords | put a section's opening words on their own line, with its contents underneath | `select` above even one column, `from` above the table, `where` above the conditions; small-query exceptions are in the rules |
| block indentation | move inward to show that something belongs inside something else | a CTE's query sits one level inside its parentheses |
| construct-relative indentation | calculate that inward step from the surrounding SQL structure, not the file's left edge | table under `from`, join one level deeper, its conditions another level deeper; normally four spaces per level |
| construct-relative indentation | calculate that inward step from the surrounding SQL structure, not the file's left edge | table and joins aligned under `from`, conditions one level deeper; normally four spaces per level |
| vertical alignment | add spaces so related items line up in columns | all output aliases in a SELECT list have their `=` signs in the same column, sized to the longest alias |
| leading boolean operators | start each additional condition with its joining word | `and o.shippeddate is not null`; preserve parentheses and the order of `and`/`or` |
| CTE chain | give intermediate queries names so a longer query can be read in stages | `;with gd as (...)` followed by `, totals as (...)`; CTE means common table expression, not a saved table |
| CTE chain | give intermediate queries names so a longer query can be read in stages | `;with gd as (...)` continues on its closing line as `), totals as (`; CTE means common table expression, not a saved table |
| comment-friendly layout | give columns and conditions their own lines so they are easier to inspect or temporarily comment out | leading commas and `and` help with later items; the first item still needs care |
| why-comments | explain a reason the SQL alone cannot tell the reader | explain why a cutoff exists, not that `where` filters rows |

Expand Down
Loading