Skip to content

feat(plugin-mssql): browse SQL Server alias, table and CLR types - #2856

Merged
datlechin merged 4 commits into
mainfrom
feat/mssql-user-defined-types
Sep 15, 2026
Merged

datlechin merged 4 commits into
mainfrom
feat/mssql-user-defined-types

Conversation

@datlechin

Copy link
Copy Markdown
Member

Stacked on #2855, which fixes the session that made SQL Server's Procedures and Functions lists empty. This adds the third thing the same user asked for: Types.

Until now fetchUserDefinedTypes had exactly one implementation, PostgreSQL's, and supportsUserDefinedTypeBrowse was true for PostgreSQL and PGlite alone. SQL Server has three shapes of user-defined type and browsed none of them.

What SQL Server has, and why the existing kinds did not fit

SQL Server Statement Closest existing kind
Alias type CREATE TYPE x FROM nvarchar(320) NOT NULL domain
Table type CREATE TYPE x AS TABLE (...) composite
CLR type CREATE TYPE x EXTERNAL NAME asm.Class none

The shapes of the first two do line up, but the words do not: a SQL Server reader has never met a domain or a composite, and labelling their types with PostgreSQL's vocabulary would be wrong on screen rather than merely imprecise. PluginUserDefinedTypeKind gains aliasType, tableType and clrType.

That enum is not @frozen and PluginObjectMapping already carries @unknown default, which is the growth path CLAUDE.md prescribes for a vocabulary like this (the same one PluginCapability uses). So the change is additive: currentPluginKitVersion 30 to 31 and the 40 plugin Info.plists, minimumCompatiblePluginKitVersion left at 19, no release-all-plugins.sh.

An earlier draft replaced the enum with a string-backed struct beside a retained legacy enum, on the argument that a closed enum forces a kit bump per engine. The review pass pushed back with the CLAUDE.md rule above and was right: the parallel vocabulary was more machinery for no benefit the @unknown default path does not already give.

Reading a definition

A SQL Server type has no sys.sql_modules row of any kind, so unlike a procedure there is no stored text to read back. Every statement is synthesized:

  • Alias: base type plus nullability. Nullability is always spelled out, because the default moves with ANSI_NULL_DFLT_ON and a reader cannot tell which applied.
  • Table: columns and indexes, keeping IDENTITY, DEFAULT, computed columns, inline indexes, and any collation that differs from the database's. A single-column primary key goes inline; a composite one gets its own clause. The constraint's own name is server-generated (PK__TT_IdLis__3214EC07...) and is deliberately dropped.
  • CLR: names the assembly, because the body is compiled .NET and no catalog read can produce it.

Both synthesized shapes were executed against SQL Server 2022 and accepted before the Swift was written, so the builder is a transcription of statements known to parse.

The max_length traps are handled and measured: it is in bytes, so nvarchar(320) reports 640 and is halved, and -1 means MAX.

Two defects the review caught before they shipped

A table type would have been offered as a column type. UserDefinedTypeSuggestions.entries had no kind filter, so once the driver returned table types the column picker would list them and SQL Server would reject every resulting ALTER TABLE. A table type is a table-valued parameter and nothing else. It is filtered out, and the driver gives it no columnTypeSpelling at all so nothing downstream can offer it either.

The adapter never backfilled a type's schema. fetchRoutines adopts the resolved schema onto every routine; fetchUserDefinedTypes did not, so a driver that left it nil produced types with a bare qualified name. Unreachable while PostgreSQL was the only implementation, and reachable the moment a second one landed. Fixed once in the adapter rather than worked around per driver.

Presentation

One flat Types section beside Procedures, Functions and Triggers, with the kind in the row's detail. No nested tree: SSMS buries a table type four levels deep under Programmability, which breaks the HIG's two-level sidebar guidance, and TablePro's flat sections are already the better shape. TablePlus has no Types section for any engine, so there was nothing to match.

All three kinds open the existing read-only source tab. An earlier draft had table types open a Structure grid of their columns; the review pass showed that meant a new TableType case on a closed enum plus editing machinery (StructureChangeManager emits ALTER TABLE, which is invalid for a type) for a read-only view, so they use the tab every other type kind already uses, carrying the synthesized statement.

Verification

  • verify.sh build: PASS
  • verify.sh test MSSQLTypeQueryTests MSSQLTypeDefinitionTests MSSQLObjectQueryTests: PASS, 25 of 25
  • xcodebuild -scheme MSSQLDriver: BUILD SUCCEEDED, 0 errors. Run directly because MSSQL is registry-only, so PR CI never compiles it, and the AllPlugins aggregate cannot run on this machine: it fails in the vendored oracle-nio with unknown attribute 'usableFromInlinenonisolated' from a @TaskLocal macro expansion, which is a toolchain bug seen on unrelated branches and is not touched by this change.
  • docs/scripts/check-writing-style.sh and check-docs-against-source.py: PASS. The second earned its keep: the kit bump silently falsified three version claims in plugin-development.mdx and plugin-registry.mdx, which are fixed here.
  • swiftlint --strict on all 9 changed Swift files: clean.

Measured through the real driver

The plugin's own sources were compiled with swiftc against the built TableProPluginKit.framework and run against SQL Server 2022, so this is MSSQLPluginDriver itself and not the query strings:

=== dbo: 4 types ===
  EmailAddress   | kind=aliasType | base=nvarchar(320) | column=[dbo].[EmailAddress]
  Money2         | kind=aliasType | base=decimal(18,4) | column=[dbo].[Money2]
  OrderLineTable | kind=tableType | base=-             | column=(not a column type)
  a<b&c>         | kind=aliasType | base=int           | column=[dbo].[a<b&c>]

--- dbo.OrderLineTable (tableType) ---
CREATE TYPE [dbo].[OrderLineTable] AS TABLE (
    [LineId] int IDENTITY(1,1) NOT NULL PRIMARY KEY,
    [Sku] varchar(32) NOT NULL,
    [Qty] int NOT NULL DEFAULT ((1)),
    [Price] decimal(18,4) NULL,
    [Note] nvarchar(200) COLLATE Latin1_General_BIN2 NULL,
    [Total] AS ([Qty]*[Price]),
    INDEX [ix_sku] NONCLUSTERED ([Sku])
);

That matches the fixture's original CREATE TYPE statement, including the collation on Note and its absence on Sku, whose collation equals the database's.

No UI automation: the Types section needs a live SQL Server, which TableProUITests has no deterministic access to.


https://claude.ai/code/session_016PSQC4EGqkpq32cWdPVMBJ

@datlechin

Copy link
Copy Markdown
Member Author

Review round: 7 findings, all real, all fixed

A second model (Codex) reviewed the branch. Every finding held up when measured against SQL Server 2022, so all seven are fixed in e7234e25e.

One broke the test suite. UserDefinedTypeToolSchemaTests pins the MCP list_types kind vocabulary exactly, and the three new enum cases failed it. Running the seven suites that own UserDefinedTypeInfo then surfaced a second break the review had not mentioned: one of my own assertions, invalidated by the fix for a different finding. Both are corrected, and the run is now 132 of 132.

Six were wrong DDL, each verified against a fixture built to exercise it:

Defect Measured
A UDT column emitted bare Email resolves to bt_is_udt=1, bt_schema=dbo; now [dbo].[EmailAddress]
NONCLUSTERED primary key replayed clustered type_desc=NONCLUSTERED was discarded on both key forms
DESC index keys lost is_descending_key=1 had nowhere to live in a comma-joined string
A comma in a column name split one column into two [a,b] is a legal identifier
CHECK constraints dropped CK__TT_HardTy__Amoun__5CD6CB2B = ([Amount]>=(0)), never read
CLR EXTERNAL NAME used the SQL type name the managed class is sys.assembly_types.assembly_class and may differ

Memory-optimized table types now keep WITH (MEMORY_OPTIMIZED = ON) and hash indexes their BUCKET_COUNT.

Index keys are read as structured rows instead of a FOR XML PATH aggregate, since that string could carry neither a direction nor a comma-bearing identifier.

Round-trip proof

The rebuilt statement was executed and the resulting type compared against the original in the catalog:

Email            -> EmailAddress   (UDT preserved)
primary key      -> NONCLUSTERED, is_primary_key = 1
ix_desc          -> NONCLUSTERED, is_descending_key = 1

Before this round the same type would have come back with a bare column type, a clustered key, an ascending index and no CHECK.

  • verify.sh build: PASS
  • 10 suites, including all 7 that own UserDefinedTypeInfo: 132 of 132
  • swiftlint --strict: clean

Base automatically changed from fix/mssql-session-options to main September 15, 2026 00:43
Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
@mintlify

mintlify Bot commented Sep 15, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 15, 2026, 12:45 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@datlechin
datlechin merged commit 6ab9d88 into main Sep 15, 2026
5 checks passed
@datlechin
datlechin deleted the feat/mssql-user-defined-types branch September 15, 2026 00:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant