Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 3 additions & 20 deletions .github/workflows/elixir.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,40 +24,25 @@ jobs:
matrix:
mssql:
- version: "mssql/server:2017-latest"
- version: "mssql/server:2022-latest"
- version: "mssql/server:2025-latest"
- version: "azure-sql-edge:latest"
pair:
- otp: 24.3
elixir: 1.14.3
include:
- mssql:
version: "mssql/server:2022-latest"
version: "mssql/server:2025-latest"
pair:
otp: 25.3
elixir: 1.18.4
lint: lint
- mssql:
version: "mssql/server:2022-latest"
pair:
otp: 24.3
elixir: 1.14.3
lint: skip_lint

env:
ACCEPT_EULA: Y
MIX_ENV: test
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

steps:
- name: Install MSSql Client Tools
run: |
curl -sSL https://packages.microsoft.com/keys/microsoft.asc \
| sudo gpg --dearmor --yes -o /usr/share/keyrings/microsoft-prod.gpg
echo "deb [arch=amd64,arm64,armhf signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/ubuntu/22.04/prod jammy main" \
| sudo tee /etc/apt/sources.list.d/mssql-release.list
sudo apt-get update
sudo apt-get install mssql-tools18 unixodbc-dev

- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1

- name: Setup elixir
Expand Down Expand Up @@ -90,6 +75,4 @@ jobs:
if: ${{ matrix.lint == 'lint' }}

- name: Run Tests
run: |
export PATH=/opt/mssql-tools18/bin:$PATH
mix test
run: mix test
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## v2.4.0 (2026-08-19)
### Fixes
* Fix `unique_constraint` support in Ecto for en-US error messages: MSSQL error metadata is now preserved when re-raising from `query!/4`, `prepare!/3`, `execute!/4` and `close!/2` (#173)
* Fix connection process crash when the server responds with an error to `sp_unprepare`, e.g. calling `close!/2` on an already closed statement

## v2.3.8 (2026-05-18)
### Improvements
* Relax decimal dependency
Expand Down
4 changes: 0 additions & 4 deletions lib/tds.ex
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ defmodule Tds do

case DBConnection.prepare_execute(conn, query, params, opts) do
{:ok, _query, result} -> result
{:error, %{mssql: %{msg_text: msg}}} -> raise Tds.Error, msg
{:error, err} -> raise err
end
end
Expand Down Expand Up @@ -125,7 +124,6 @@ defmodule Tds do

case DBConnection.prepare(conn, query, opts) do
{:ok, query} -> query
{:error, %{mssql: %{msg_text: msg}}} -> raise Tds.Error, msg
{:error, err} -> raise err
end
end
Expand All @@ -145,7 +143,6 @@ defmodule Tds do
def execute!(conn, query, params, opts \\ []) do
case DBConnection.execute(conn, query, params, opts) do
{:ok, _q, result} -> result
{:error, %{mssql: %{msg_text: msg}}} -> raise Tds.Error, msg
{:error, err} -> raise err
end
end
Expand All @@ -162,7 +159,6 @@ defmodule Tds do
def close!(conn, query, opts \\ []) do
case DBConnection.close(conn, query, opts) do
{:ok, result} -> result
{:error, %{mssql: %{msg_text: msg}}} -> raise Tds.Error, msg
{:error, err} -> raise err
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/tds/errors.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
unique,2627,Violation of UNIQUE KEY constraint '%.*ls'\..*
unique,2627,Violation of UNIQUE KEY constraint '(\S+?)'\..*
unique,2627,Verletzung der UNIQUE KEY-Einschränkung "(\S+?)"\..*
unique,2627,Violation de la contrainte UNIQUE KEY « (\S+?) »\..*
unique,2627,制約 '(\S+?)' の UNIQUE KEY 違反.*
Expand Down
2 changes: 1 addition & 1 deletion lib/tds/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ defmodule Tds.Protocol do
| {:error | :disconnect, Exception.t(), new_state :: t()}
def handle_close(query, opts, s) do
params = opts[:parameters]
send_close(query, params, s)
send_close(query, params, %{s | state: :executing})
end

@spec handle_begin(Keyword.t(), t) ::
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Tds.Mixfile do
use Mix.Project

@source_url "https://github.com/elixir-ecto/tds"
@version "2.3.8"
@version "2.4.0"

def project do
[
Expand Down
18 changes: 18 additions & 0 deletions test/error_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,22 @@ defmodule ErrorTest do
raise Tds.Error, profession: "crocodile hunter"
end
end

test "extracts unique constraint name from en-US error 2627" do
message =
"Violation of UNIQUE KEY constraint 'UQ_users_email'. Details: key violation (key value string = (email=foo@bar.com))."

assert [unique: "UQ_users_email"] = Tds.Error.get_constraint_violations(2627, message)
end

test "extracts unique index name from en-US error 2601" do
message =
"Cannot insert duplicate key row in object 'dbo.users' with unique index 'IX_users_email'. The duplicate key value is (foo@bar.com)."

assert [unique: "IX_users_email"] = Tds.Error.get_constraint_violations(2601, message)
end

test "returns no constraints for unknown error code" do
assert [] = Tds.Error.get_constraint_violations(9999, "some error")
end
end
117 changes: 117 additions & 0 deletions test/query_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,123 @@ defmodule QueryTest do
assert [[1]] = query("SELECT 1", [])
end

test "query returns Tds.Error with MSSQL error metadata on unique violation", context do
query("DROP TABLE UniqueBangTable", [])

assert :ok =
query(
"CREATE TABLE UniqueBangTable (id INT, email NVARCHAR(100), " <>
"CONSTRAINT UQ_UniqueBangTable_email UNIQUE (email))",
[]
)

assert :ok = query("INSERT INTO UniqueBangTable (id, email) VALUES (1, 'foo@bar.com')", [])

err = query("INSERT INTO UniqueBangTable (id, email) VALUES (2, 'foo@bar.com')", [])
assert %{number: 2627, msg_text: msg} = err.mssql
assert msg =~ "UQ_UniqueBangTable_email"

query("DROP TABLE dbo.UniqueBangTable", [])
end

test "query! re-raises Tds.Error preserving MSSQL error metadata", context do
pid = context[:pid]

query("DROP TABLE UniqueBangTable2", [])

assert :ok =
query(
"CREATE TABLE UniqueBangTable2 (id INT, email NVARCHAR(100), " <>
"CONSTRAINT UQ_UniqueBangTable2_email UNIQUE (email))",
[]
)

assert :ok = query("INSERT INTO UniqueBangTable2 (id, email) VALUES (1, 'foo@bar.com')", [])

err =
try do
Tds.query!(pid, "INSERT INTO UniqueBangTable2 (id, email) VALUES (2, 'foo@bar.com')", [])
flunk("expected Tds.Error to be raised")
rescue
e in Tds.Error -> e
end

assert %{number: 2627, msg_text: msg} = err.mssql
assert msg =~ "UQ_UniqueBangTable2_email"

query("DROP TABLE dbo.UniqueBangTable2", [])
end

test "prepare! re-raises Tds.Error preserving MSSQL error metadata", context do
pid = context[:pid]

err =
try do
Tds.prepare!(pid, "SELECT * FROM UniqueBangPrepareMissingTable", [])
flunk("expected Tds.Error to be raised")
rescue
e in Tds.Error -> e
end

assert %{number: 208, msg_text: msg} = err.mssql
assert msg =~ "UniqueBangPrepareMissingTable"
end

test "execute! re-raises Tds.Error preserving MSSQL error metadata on unique violation",
context do
pid = context[:pid]

query("DROP TABLE UniqueBangExecuteTable", [])

assert :ok =
query(
"CREATE TABLE UniqueBangExecuteTable (id INT, email NVARCHAR(100), " <>
"CONSTRAINT UQ_UniqueBangExecuteTable_email UNIQUE (email))",
[]
)

assert :ok =
query("INSERT INTO UniqueBangExecuteTable (id, email) VALUES (1, 'foo@bar.com')", [])

{:ok, q} =
Tds.prepare(
pid,
"INSERT INTO UniqueBangExecuteTable (id, email) VALUES (2, 'foo@bar.com')",
[]
)

err =
try do
Tds.execute!(pid, q, [], [])
flunk("expected Tds.Error to be raised")
rescue
e in Tds.Error -> e
end

assert %{number: 2627, msg_text: msg} = err.mssql
assert msg =~ "UQ_UniqueBangExecuteTable_email"

query("DROP TABLE dbo.UniqueBangExecuteTable", [])
end

test "close! re-raises Tds.Error preserving MSSQL error metadata", context do
pid = context[:pid]

{:ok, q} = Tds.prepare(pid, "SELECT 1", [])
assert %Tds.Result{} = Tds.close!(pid, q, [])

err =
try do
Tds.close!(pid, q, [])
flunk("expected Tds.Error to be raised")
rescue
e in Tds.Error -> e
end

assert %{number: 8179, msg_text: msg} = err.mssql
assert msg =~ "Could not find prepared statement"
end

test "char nulls", context do
assert [[nil]] = query("SELECT CAST(NULL as nvarchar(255))", [])
end
Expand Down
Loading