|
| 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