Skip to content

Commit 158f7c2

Browse files
committed
LDEV-5972 add modern mssql support recipe
1 parent b019653 commit 158f7c2

File tree

1 file changed

+218
-0
lines changed

1 file changed

+218
-0
lines changed

docs/recipes/mssql-modern-mode.md

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
<!--
2+
{
3+
"title": "Lucee MSSQL Modern Mode",
4+
"id": "mssql-modern-mode",
5+
"since": "5.3.8.169",
6+
"categories": ["database", "mssql"],
7+
"description": "How to enable and use MSSQL modern mode in Lucee for proper handling of RAISERROR exceptions and complex T-SQL batches",
8+
"keywords": [
9+
"mssql",
10+
"sql server",
11+
"raiserror",
12+
"deferred exceptions",
13+
"modern mode",
14+
"jdbc",
15+
"stored procedures",
16+
"t-sql"
17+
],
18+
"related": [
19+
"database-connection-management",
20+
"datasource-configuration",
21+
"tag-query",
22+
"function-queryexecute",
23+
"tag-storedproc"
24+
]
25+
}
26+
-->
27+
28+
# MSSQL Modern Mode
29+
30+
Microsoft SQL Server's JDBC driver has unique behaviour that differs from other database drivers. Lucee provides a "modern mode" that properly handles these quirks, particularly around deferred exceptions like RAISERROR.
31+
32+
## The Problem
33+
34+
The MSSQL JDBC driver queues certain exceptions (like those from RAISERROR) and only surfaces them when you iterate through all result sets. Without proper handling, these exceptions can be silently ignored, leading to:
35+
36+
- RAISERROR statements not throwing exceptions in CFML
37+
- Stored procedure errors going undetected
38+
- Silent data corruption when validation errors are missed
39+
40+
### Example of the Problem
41+
42+
```sql
43+
-- SQL Server stored procedure
44+
CREATE PROCEDURE ValidateAndInsert @value INT
45+
AS
46+
BEGIN
47+
IF @value < 0
48+
RAISERROR('Value cannot be negative!', 16, 1);
49+
50+
INSERT INTO MyTable (value) VALUES (@value);
51+
END
52+
```
53+
54+
Without modern mode enabled, calling this procedure with a negative value might:
55+
56+
1. Execute the RAISERROR (queueing the exception)
57+
2. Still execute the INSERT
58+
3. Return successfully to CFML without throwing an error
59+
60+
## Enabling Modern Mode
61+
62+
Enable MSSQL modern mode by setting a system property or environment variable:
63+
64+
### System Property
65+
66+
```bash
67+
-Dlucee.datasource.mssql.modern=true
68+
```
69+
70+
### Environment Variable
71+
72+
```bash
73+
LUCEE_DATASOURCE_MSSQL_MODERN=true
74+
```
75+
76+
### In Docker
77+
78+
```dockerfile
79+
ENV LUCEE_DATASOURCE_MSSQL_MODERN=true
80+
```
81+
82+
Or via JVM args:
83+
84+
```dockerfile
85+
ENV LUCEE_JAVA_OPTS="-Dlucee.datasource.mssql.modern=true"
86+
```
87+
88+
## What Modern Mode Does
89+
90+
When enabled, Lucee uses a specialised execution path for MSSQL queries that:
91+
92+
1. **Iterates through all result sets** - Ensures deferred exceptions surface by calling `getMoreResults()` until no more results exist
93+
2. **Properly handles RAISERROR** - Exceptions with severity 10+ are thrown as CFML exceptions
94+
3. **Supports complex T-SQL batches** - Multiple statements, OUTPUT clauses, and interleaved results are handled correctly
95+
96+
## When to Use Modern Mode
97+
98+
Enable modern mode if your application:
99+
100+
- Uses stored procedures with RAISERROR or THROW statements
101+
- Relies on T-SQL validation that raises errors
102+
- Uses complex batches with multiple statements
103+
- Uses INSERT/UPDATE with OUTPUT clauses
104+
- Needs reliable error handling from SQL Server
105+
106+
## Code Examples
107+
108+
### RAISERROR Handling
109+
110+
```javascript
111+
// With modern mode enabled, this will throw an exception
112+
try {
113+
queryExecute("
114+
SELECT 1 as result;
115+
RAISERROR('Something went wrong!', 16, 1);
116+
", {}, { datasource: "mssql" });
117+
} catch (database e) {
118+
writeOutput("Caught error: " & e.message);
119+
// Output: "Caught error: Something went wrong!"
120+
}
121+
```
122+
123+
### Stored Procedure Errors
124+
125+
```javascript
126+
// Stored procedure with validation
127+
try {
128+
queryExecute("EXEC ValidateAndInsert @value = :val",
129+
{ val: -5 },
130+
{ datasource: "mssql" }
131+
);
132+
} catch (database e) {
133+
writeOutput("Validation failed: " & e.message);
134+
}
135+
```
136+
137+
### INSERT with OUTPUT Clause
138+
139+
```javascript
140+
// Modern mode properly handles OUTPUT clause results
141+
var result = queryExecute("
142+
INSERT INTO Users (name, email)
143+
OUTPUT INSERTED.id, INSERTED.created_at
144+
VALUES (:name, :email)
145+
", {
146+
name: "John Doe",
147+
148+
}, {
149+
datasource: "mssql",
150+
result: "info"
151+
});
152+
153+
// result contains the OUTPUT data
154+
writeOutput("New user ID: " & result.id);
155+
writeOutput("Generated key: " & info.generatedKey);
156+
```
157+
158+
## RAISERROR Severity Levels
159+
160+
SQL Server's RAISERROR uses severity levels to indicate error type:
161+
162+
| Severity | Behaviour |
163+
|----------|-----------|
164+
| 0-9 | Informational - becomes SQLWarning (not thrown) |
165+
| 10 | Informational - becomes SQLWarning (not thrown) |
166+
| 11-16 | User errors - thrown as SQLException |
167+
| 17-19 | Resource/software errors - thrown as SQLException |
168+
| 20-25 | Fatal errors - connection terminated |
169+
170+
For errors to be caught in CFML, use severity 11 or higher (16 is most common for user errors):
171+
172+
```sql
173+
-- This WILL throw an exception (severity 16)
174+
RAISERROR('User error!', 16, 1);
175+
176+
-- This will NOT throw (severity 10, informational only)
177+
RAISERROR('Just a notice', 10, 1);
178+
```
179+
180+
## Performance Considerations
181+
182+
Modern mode adds minimal overhead:
183+
184+
- For simple SELECT queries: negligible impact
185+
- For INSERT/UPDATE: ensures all results are consumed (required for proper cleanup anyway)
186+
- For complex batches: necessary overhead to detect deferred errors
187+
188+
The performance cost is far outweighed by the correctness benefits of proper error handling.
189+
190+
## Compatibility
191+
192+
- **Lucee Version**: 5.3.8.169+ (feature added in [LDEV-3127](https://luceeserver.atlassian.net/browse/LDEV-3127), fixed in [LDEV-5970](https://luceeserver.atlassian.net/browse/LDEV-5970))
193+
- **MSSQL JDBC Driver**: Tested with versions 9.x through 13.x
194+
- **SQL Server**: Works with SQL Server 2012 and later
195+
196+
The implementation was improved in `6.2.5.7` and `7.0.2.8` as part of [LDEV-5970](https://luceeserver.atlassian.net/browse/LDEV-5970) and [LDEV-5972](https://luceeserver.atlassian.net/browse/LDEV-5972)
197+
198+
## Troubleshooting
199+
200+
### Errors Not Being Caught
201+
202+
1. Verify modern mode is enabled: check system properties/environment
203+
2. Ensure RAISERROR severity is 11 or higher
204+
3. Check that you're catching the correct exception type (`database` or `any`)
205+
206+
### "Result set is closed" Errors
207+
208+
If you see this error with older Lucee versions, upgrade to 6.2+ or 6.1.1+ where this was fixed (LDEV-5970).
209+
210+
### Stored Procedure Returns No Data
211+
212+
Some stored procedures return multiple result sets. Modern mode processes all of them but only returns the first. Use OUTPUT parameters or restructure your procedure if you need all results.
213+
214+
## Related Resources
215+
216+
- [Microsoft JDBC Driver Documentation](https://learn.microsoft.com/en-us/sql/connect/jdbc/using-multiple-result-sets)
217+
- [RAISERROR (Transact-SQL)](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/raiserror-transact-sql)
218+
- [Database Connection Management](database-connection-management.md)

0 commit comments

Comments
 (0)