Skip to content
PYD ENTERPRISES

Migration guide

T-SQL stored procedures to .NET services: return codes, NULLs and hidden writes

SQL Server procedures encode rules in return codes, @@ROWCOUNT checks and three-valued logic. This guide shows how to surface those rules and their side effects so .NET services reproduce them deliberately.

Microsoft T-SQL.NET services
DIFFICULTY
High
OUTPUT
Rule / schema JSON
SYNTAX TRAPS
5 covered
WORKFLOW
6 steps

By Pranav Kulkarni, Head of Language Engineering · Updated 15 Oct 2026

Technical syntax challenges

Where Microsoft T-SQL migrations go wrong.

01

NULL makes conditions silently false

IF @Used + @Amount > @Limit is UNKNOWN when @Used is NULL, so the IF branch is skipped. Procedures written years ago relied on this by accident. Extraction must evaluate three-valued logic and list every condition that a NULL can bypass.

02

Return codes and @@ROWCOUNT

RETURN 2 and IF @@ROWCOUNT = 0 RETURN 1 are the procedure's result contract. They are consumed by callers that branch on them, so each value needs a name and a documented meaning in the service API.

03

Error handling varies by era

Older code checks @@ERROR after each statement; newer code uses TRY/CATCH with XACT_ABORT ON. Without XACT_ABORT, many runtime errors abort only the failing statement and the transaction continues. The spec records which writes survive which errors.

04

Temp tables and set-based triggers

#temp tables carry intermediate results between statements and sometimes between nested procedures. Triggers see multi-row inserted and deleted pseudo-tables; a trigger written for single rows (SELECT @id = id FROM inserted) mishandles batch updates, and that bug is now behavior.

05

Type quirks: money and datetime

money is a fixed-point type with four decimal places (8 bytes), best mapped to decimal(19,4). Legacy datetime is accurate to increments of .000, .003 or .007 seconds, so timestamps round-trip differently through DateTime and datetime2.

Extraction workflow

6 steps from source to signed-off spec.

The sequence a PYD ENTERPRISES Discovery Scan runs for this stack. Every output links back to source file and line range.

  1. 1

    Script the database objects

    Export tables, views, procedures, functions and triggers, plus the ANSI_NULLS and QUOTED_IDENTIFIER settings each object was created with.

  2. 2

    Parse T-SQL and resolve temp table scope

    PYD ENTERPRISES parses each object, resolves #temp tables across nested procedure calls and records the column types of each table variable.

  3. 3

    Evaluate conditions with three-valued logic

    Every predicate is checked for nullable operands against column nullability, and conditions a NULL can bypass are flagged as rules needing confirmation.

  4. 4

    Link DML to triggers and callers

    Each INSERT, UPDATE, DELETE and MERGE is joined to the triggers it fires; callers from application code are traced where the .NET or VB6 source is ingested.

  5. 5

    Extract decisions, return codes and side effects

    Decisions become DMN tables, return codes become a named result contract and each procedure gets a read and write inventory with transaction boundaries.

  6. 6

    Export service contracts and fixtures

    OpenAPI 3.1 operations and JSON fixtures, including NULL and concurrency cases, are exported for the .NET team to build and test against.

Legacy in, specification out

The source, and what PYD ENTERPRISES extracts from it.

Left: the Microsoft T-SQL construct as it sits in production. Right: the reviewed artifact your .NET services engineers build against. PYD ENTERPRISES produces the spec and the tests; your team writes the service.

LEGACY SOURCE

usp_ApplyCredit.sqlSQL
1CREATE PROCEDURE dbo.usp_ApplyCredit
2 @CustomerId INT,
3 @Amount MONEY
4AS
5BEGIN
6 SET NOCOUNT ON;
7 DECLARE @Limit MONEY, @Used MONEY;
8 SELECT @Limit = CreditLimit, @Used = CreditUsed
9 FROM dbo.Customer
10 WHERE CustomerId = @CustomerId;
11 IF @@ROWCOUNT = 0 RETURN 1;
12 IF @Used + @Amount > @Limit
13 BEGIN
14 INSERT INTO dbo.CreditHold (CustomerId, Amount, HeldAt)
15 VALUES (@CustomerId, @Amount, GETDATE());
16 RETURN 2;
17 END
18 UPDATE dbo.Customer
19 SET CreditUsed = CreditUsed + @Amount
20 WHERE CustomerId = @CustomerId;
21 RETURN 0;
22END

EXTRACTED ARTIFACT

usp_ApplyCredit.spec.jsonJSON
1{
2 "procedure": "dbo.usp_ApplyCredit",
3 "source": "usp_ApplyCredit.sql:1-22",
4 "result": {
5 "0": "CREDIT_APPLIED",
6 "1": "CUSTOMER_NOT_FOUND",
7 "2": "OVER_LIMIT_HOLD_RECORDED"
8 },
9 "decision": {
10 "hitPolicy": "FIRST",
11 "rules": [
12 { "when": "customer missing", "then": 1 },
13 { "when": "used + amount > limit", "then": 2 },
14 { "when": "otherwise", "then": 0 }
15 ]
16 },
17 "reads": ["dbo.Customer.CreditLimit", "dbo.Customer.CreditUsed"],
18 "writes": ["dbo.Customer.CreditUsed", "dbo.CreditHold"],
19 "types": { "Amount": "money, decimal(19,4)" },
20 "review": [
21 "CreditUsed NULL: limit check is UNKNOWN, skipped; CreditUsed stays NULL",
22 "No transaction or lock between SELECT and UPDATE: concurrent calls can both pass"
23 ]
24}

Architectural best practices

Build the .NET services side to the spec, not to the old shape.

  • Name every return code and expose it as a typed result in the .NET service, not an integer.
  • Decide NULL behavior explicitly for every flagged condition, and add NOT NULL constraints or defaults where the business agrees.
  • Wrap check-then-update logic in one transaction with the right isolation or an atomic conditional UPDATE, and test it under concurrency.
  • Keep set-based operations in SQL where they belong; move the decisions and orchestration into services.
  • Replace trigger side effects with explicit domain events or service calls, documented in the contract.

What the example procedure actually does

Read as a spec, usp_ApplyCredit has three outcomes and two defects. The outcomes are clear: not found, over limit with a hold recorded, or applied. The defects only appear when you evaluate it rather than read it. A customer whose CreditUsed is NULL is never held, because the comparison is UNKNOWN, and their used credit stays NULL forever because NULL + @Amount is NULL. And two concurrent calls can both read the same CreditUsed and both pass the limit check.

PYD ENTERPRISES reports both as review items next to the lines that cause them. The business decides whether the new service keeps, fixes or reports them; the decision is recorded with the rule.

Where PYD ENTERPRISES stops

PYD ENTERPRISES produces decision tables, result contracts, side-effect inventories, OpenAPI 3.1 operations and fixtures. It does not generate C# services, Entity Framework models or migrations. ASP.NET Core teams usually build services against the contracts, keep some set-based procedures as they are, and use the fixtures in data-driven tests that run against both the procedure and the new service during parallel running. Pair this with the VB6 to C# guide when the procedures are called from a VB6 client.

FAQ

Microsoft T-SQL to .NET services questions

Does PYD ENTERPRISES need the SQL Server instance?

No. Scripted object definitions, including SET options, are enough for extraction. A test instance is useful for running the generated fixtures against the existing procedures.

How is dynamic SQL through sp_executesql handled?

Where the statement text is built from constants and parameters, it is parsed and analyzed like static SQL. Where it depends on unbounded runtime data, it is flagged for review with the fragments that could be resolved.

Are SQL CLR procedures supported?

SQL CLR assemblies are recorded as external calls. If you ingest their C# source, PYD ENTERPRISES analyzes them with the .NET parser and links them into the same graph.

Get the spec for your Microsoft T-SQL estate.

Request a codebase scan and review extracted rules, contracts and fixtures for your own Microsoft T-SQL estate, inside your VPC.