SeaORM 2.0 with SQL Server Support
SeaORM X is a proprietary extension of SeaORM with first-class Microsoft SQL Server support. It covers the same API surface, with the full test suite and examples ported to MSSQL. With SeaORM 2.0, SeaORM X has been rebuilt on top of it: every improvement in the open-source release ships to MSSQL users too.
If you are building enterprise software on SQL Server, you can request commercial access.
What's in SeaORM Xโ
1. Native MSSQL Driver via SQLz with Connection Poolingโ
SeaORM X ships a full ConnectionPool driver for SQL Server (mssql:// URI scheme) built on top of the SQLz/TDS stack. You get the same Database::connect(url) ergonomics as MySQL, PostgreSQL, and SQLite: no separate client setup required.
let db = Database::connect("mssql://user:pass@localhost:1433?trustCertificate=true").await?;
The underlying SQLz pool is a parallel implementation of SQLx's pool architecture applied to SQL Server. It supports the same lifecycle hooks as SQLx:
after_connect: initialize a freshly opened connectionbefore_acquire: validate an idle connection before handing it outafter_release: clean up before returning a connection to the pool
All standard PoolOptions settings are supported: max_connections, min_connections, acquire_timeout, max_lifetime, idle_timeout, test_before_acquire, etc.
2. Nested Transactions via Savepointsโ
SQLz tracks transaction depth as a counter on the connection and maps it to MSSQL's savepoint syntax transparently:
| Depth | begin | commit | rollback |
|---|---|---|---|
| 0 โ 1 | BEGIN TRAN | COMMIT TRAN | ROLLBACK TRAN |
| n โ n+1 | SAVE TRAN _sqlz_savepoint_n | (no-op: SQL Server releases savepoints implicitly) | ROLLBACK TRAN _sqlz_savepoint_n |
db.transaction(|tx| tx.transaction(|tx2| ...)) gives correct nested rollback semantics: rolling back an inner block unwinds only to the savepoint, leaving the outer transaction intact.
let txn = db.begin().await?;
{
let txn = txn.begin().await?;
let _ = bakery::ActiveModel { name: Set("Nested Bakery".to_owned()), .. }.save(&txn).await?;
assert_eq!(Bakery::find().all(&txn).await?.len(), 3);
{
let txn = txn.begin().await?;
let _ = bakery::ActiveModel { name: Set("Rock n Roll Bakery".to_owned()), .. }.save(&txn).await?;
assert_eq!(Bakery::find().all(&txn).await?.len(), 4);
// txn dropped here without commit: rolls back to savepoint
}
assert_eq!(Bakery::find().all(&txn).await?.len(), 3);
{
let txn = txn.begin().await?;
let _ = bakery::ActiveModel { name: Set("Rock n Roll Bakery".to_owned()), .. }.save(&txn).await?;
txn.commit().await?;
}
txn.commit().await?;
}
assert_eq!(Bakery::find().all(&txn).await?.len(), 4);
txn.commit().await?;
BEGIN TRAN
SAVE TRAN _sqlz_savepoint_1
SAVE TRAN _sqlz_savepoint_2
ROLLBACK TRAN _sqlz_savepoint_2 -- drop without commit
SAVE TRAN _sqlz_savepoint_2
-- (savepoint 2 released implicitly on commit)
-- (savepoint 1 released implicitly on commit)
COMMIT TRAN
When a Transaction is dropped without an explicit commit, SQLz spawns a local async task to execute ROLLBACK immediately and marks the connection as RollingBack. Any subsequent use of that connection awaits the rollback task before proceeding, making cleanup deterministic rather than deferred to an arbitrary future await point.
3. Automatic Schema Rewritingโ
ConnectionTrait gains a get_schema() hook. When a connection is configured with a non-default schema (e.g. dbo, sales), SeaORM X automatically prefixes every outgoing statement with that schema. No manual [schema].[table] boilerplate in application code.
The rewriting is backed by the OverrideTableSchema trait, implemented for every sea-query statement type. Schema rewriting propagates recursively into subqueries, JOINs, and CTE branches.
// specify schema other than dbo
let db = Database::connect(
"mssql://user:pass@localhost:1433/my_db?currentSchema=my_schema").await?;
let related = cake::Entity::find()
.has_related(filling::Entity, filling::Column::Name.eq("Marmalade"))
.all(db)
.await?;
SELECT [cake].[id], [cake].[name] FROM [my_schema].[cake]
WHERE EXISTS(SELECT 1 FROM [my_schema].[filling]
INNER JOIN [my_schema].[cake_filling] ON [cake_filling].[filling_id] = [filling].[id]
WHERE [filling].[name] = 'Marmalade'
AND [cake].[id] = [cake_filling].[cake_id])
4. OUTPUT Clause for RETURNINGโ
SeaORM X maps SeaORM's RETURNING semantics to MSSQL's OUTPUT syntax:
- INSERT โ
OUTPUT INSERTED.* - DELETE โ
OUTPUT DELETED.*
Constraint violations (UniqueConstraintViolation, ForeignKeyConstraintViolation) surfacing through the OUTPUT path are caught and returned as typed DbErr variants, not raw driver errors.
5. Automatic IDENTITY_INSERT Handlingโ
When inserting a model with an explicit primary-key value on an auto-increment column, SeaORM X automatically wraps the statement in SET IDENTITY_INSERT ON/OFF: no manual SQL required.
// From tests/empty_insert_tests.rs
let seaside_bakery = bakery::ActiveModel {
name: Set("SeaSide Bakery".to_owned()),
profit_margin: Set(10.4),
..Default::default() // id not set: normal insert
};
Bakery::insert(seaside_bakery).exec(db).await?;
let double_seaside_bakery = bakery::ActiveModel {
id: Set(1), // explicit PK: triggers IDENTITY_INSERT automatically
name: Set("SeaSide Bakery".to_owned()),
profit_margin: Set(10.4),
..Default::default()
};
let res = Bakery::insert_many([double_seaside_bakery])
.on_conflict_do_nothing()
.exec(db)
.await;
assert!(matches!(res, Ok(TryInsertResult::Conflicted)));
-- Normal insert: no IDENTITY_INSERT
INSERT INTO [bakery] ([name], [profit_margin]) OUTPUT [INSERTED].[id] VALUES ('SeaSide Bakery', 10.4)
-- Explicit PK insert: wrapped automatically
SET IDENTITY_INSERT [bakery] ON;
INSERT INTO [bakery] ([id], [name], [profit_margin]) OUTPUT [INSERTED].[id] VALUES (1, 'SeaSide Bakery', 10.4);
SET IDENTITY_INSERT [bakery] OFF
-- PK conflict โ returned as TryInsertResult::Conflicted, not a panic
6. Tuple IN Fallback for MSSQLโ
MSSQL does not support tuple value syntax ((c1, c2) IN ((v1, v2), ...)). SeaORM X adds EntityTrait::column_tuple_in(), which expands to (c1 = v1 AND c2 = v2) OR ... when targeting MSSQL.
cake::Entity::find()
.filter(cake::Entity::column_tuple_in(
[cake::Column::Id, cake::Column::Name],
&[(1i32, "a").into_value_tuple(), (2i32, "b").into_value_tuple()],
DbBackend::MySql,
).unwrap())
.build(DbBackend::MySql)
.to_string();
-- MySQL: native tuple syntax:
SELECT `cake`.`id`, `cake`.`name` FROM `cake`
WHERE (`cake`.`id`, `cake`.`name`) IN ((1, 'a'), (2, 'b'))
cake::Entity::find()
.filter(cake::Entity::column_tuple_in(
[cake::Column::Id, cake::Column::Name],
&[(1i32, "a").into_value_tuple(), (2i32, "b").into_value_tuple()],
DbBackend::MsSql,
).unwrap())
.build(DbBackend::MsSql)
.to_string();
-- MSSQL: automatically expands to AND/OR:
SELECT [cake].[id], [cake].[name] FROM [cake]
WHERE ([cake].[id] = 1 AND [cake].[name] = 'a') OR ([cake].[id] = 2 AND [cake].[name] = 'b')
7. i64 / i32 Type Coercionโ
MSSQL returns INT columns as i32 at the wire level. SeaORM X handles the coercion transparently: no schema changes or manual casting required.
8. Schema-First and Entity-First Workflowsโ
Schema-first: point sea-orm-cli generate entity at an existing MSSQL database and get ready-to-compile Rust entity files.
sea-orm-cli generate entity \
--database-url "mssql://sa:pass@localhost/AdventureWorksLT2016" \
--database-schema "SalesLT"
// Generated address.rs
#[sea_orm(schema_name = "SalesLT", table_name = "Address")]
pub struct Model {
#[sea_orm(column_name = "AddressID", primary_key)]
pub address_id: i32,
#[sea_orm(column_name = "AddressLine1")]
pub address_line1: String,
pub rowguid: Uuid,
// ...
}
Entity-first: define entities in Rust and let SeaORM sync the schema to the database. Tables, columns, unique keys, and foreign keys are created in topological order: no manual migration file needed.
// Requires the `schema-sync` feature flag
db.get_schema_builder()
.register(order::Entity)
.register(store::Entity)
.sync(db)
.await?;
9. execute_unprepared via Raw TDS Batchโ
For DDL and migration scenarios, execute_unprepared uses the raw TDS execute_batch path rather than prepared statements. This preserves temp-table visibility across statement batches: a common requirement for MSSQL migration and stored-procedure patterns that break under prepared statement isolation.
10. Built on SeaORM 2.0โ
Because SeaORM X tracks the open-source release, all SeaORM 2.0 improvements are available to MSSQL users. Highlights:
New entity format: a dense, readable layout with relations declared inline on the model struct, generated with --entity-format dense:
#[sea_orm::model]
#[sea_orm(table_name = "post")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: i32,
#[sea_orm(belongs_to, from = "user_id", to = "id")]
pub author: HasOne<super::user::Entity>,
#[sea_orm(has_many, via = "post_tag")]
pub tags: HasMany<super::tag::Entity>,
}
Smart Entity Loader: automatically chooses JOIN for 1-1 and the data-loader pattern for 1-N, eliminating the N+1 problem in nested queries.
Nested ActiveModel persistence: insert an entire object graph (user โ profile 1-1, posts 1-N, tags M-N) in a single .save(db).await? call.
Strongly-typed columns: Cake::COLUMN.name.contains("chocolate") with methods specific to the column type.
Injection-safe raw_sql! macro: use {param} interpolation and {..array} expansion in raw SQL without opening the door to SQL injection.
DerivePartialModel with #[sea_orm(nested)]: select only the fields you need across joins and map them into typed partial structs, preventing overfetching.
Limitationsโ
SQLz is a purpose-built SQL Server driver, not a full SQLx port. Known gaps:
- No compile-time query checking. SQLx's
sqlx::query!macro verifies SQL against a live database at compile time. SQLz has no equivalent; queries are checked at runtime only. - No custom type encoding/decoding. SQLx supports user-defined
Encode/Decodeimplementations for arbitrary Rust types. SQLz ships a fixed set of supported types (primitives,uuid,Decimal,BigDecimal,chrono,time,serde_json) behind feature flags. Adding a new wire type requires changes inside SQLz itself.
Getting Accessโ
SeaORM X is a commercial offering for teams that need SQL Server support in Rust without leaving the SeaORM ecosystem. It has been tested against complex schemas including Sakila and AdventureWorks, and is used in production by companies today.
The practical upside for teams already on SeaORM: there is no second API to learn or maintain. Queries, relations, transactions all carry over unchanged. Developers can be productive on day one, and adding SQL Server to an existing multi-database project does not require parallel ORM maintenance or separate migration tooling.
For greenfield projects on legacy MSSQL schemas, the CLI codegen eliminates the mechanical work of writing entity files by hand.
SeaORM X is distributed under a commercial license. To evaluate it or discuss access for your team, request access here.
