diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index d2ce63ce..c8b9d039 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -24,24 +24,18 @@ 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 @@ -49,15 +43,6 @@ jobs: 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 @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2098ba0a..4b2d12c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/tds.ex b/lib/tds.ex index caa0cc10..bf3e7d1b 100644 --- a/lib/tds.ex +++ b/lib/tds.ex @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/lib/tds/errors.csv b/lib/tds/errors.csv index 45281c5a..7959acf5 100644 --- a/lib/tds/errors.csv +++ b/lib/tds/errors.csv @@ -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 違反.* diff --git a/lib/tds/protocol.ex b/lib/tds/protocol.ex index 2fc918e1..f89e1534 100644 --- a/lib/tds/protocol.ex +++ b/lib/tds/protocol.ex @@ -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) :: diff --git a/mix.exs b/mix.exs index c26cc9a6..a58ff1ac 100644 --- a/mix.exs +++ b/mix.exs @@ -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 [ diff --git a/test/error_test.exs b/test/error_test.exs index 8dceb835..9d154729 100644 --- a/test/error_test.exs +++ b/test/error_test.exs @@ -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 diff --git a/test/query_test.exs b/test/query_test.exs index 3544493e..ec3ce667 100644 --- a/test/query_test.exs +++ b/test/query_test.exs @@ -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