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
167 changes: 167 additions & 0 deletions .github/workflows/build-test-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
name: Build, Test & Publish
permissions:
contents: write

on:
push:
branches: [master]
pull_request:

defaults:
run:
shell: bash

jobs:
build:
name: Build & Test (${{ matrix.mssql_image }})
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
mssql_image:
- mcr.microsoft.com/mssql/server:2025-latest
- mcr.microsoft.com/mssql/server:2022-latest
- mcr.microsoft.com/mssql/server:2019-latest
- mcr.microsoft.com/mssql/server:2017-latest

env:
MIX_ENV: test
MSSQL_IMAGE: ${{ matrix.mssql_image }}

steps:
- uses: actions/checkout@v4

- uses: jdx/mise-action@v2

- name: Install dependencies
run: mix deps.get

- name: Compile
run: mix compile --warnings-as-errors

- name: Static analysis
run: mix credo

- name: Run integration tests
run: mix test --only integration

benchmark:
name: Benchmark
runs-on: ubuntu-latest
needs: build
if: github.ref_name == github.event.repository.default_branch && !failure() && !cancelled()
# Non-blocking: a flaky benchmark must never prevent a release from shipping.
continue-on-error: true

env:
MIX_ENV: dev
MSSQL_IMAGE: mcr.microsoft.com/mssql/server:2022-latest

steps:
- uses: actions/checkout@v4

- uses: jdx/mise-action@v2

- name: Cache Mix dependencies
uses: actions/cache@v4
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-dev-${{ hashFiles('mix.lock') }}
restore-keys: ${{ runner.os }}-mix-dev-

- name: Install dependencies
run: mix deps.get

- name: Compile
run: mix compile

- name: Run benchmarks
# stderr carries Logger/Testcontainers debug noise; stdout has the results.
run: mix run bench/benchmarks.exs > bench_results.txt 2>/dev/null

- name: Upload benchmark results
uses: actions/upload-artifact@v4
with:
name: benchmark-results
path: bench_results.txt

publish:
name: Publish
runs-on: ubuntu-latest
needs: [build, benchmark]
if: github.ref_name == github.event.repository.default_branch && !failure() && !cancelled()

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: jdx/mise-action@v2

- name: Cache Mix dependencies
uses: actions/cache@v4
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-${{ hashFiles('mix.lock') }}
restore-keys: ${{ runner.os }}-mix-

- name: Install dependencies
run: mix deps.get

- name: Get version
id: version
run: |
BASE=$(cat VERSION)
BASE_COMMIT=$(git log -1 --format="%H" -- VERSION)
HEIGHT=$(git rev-list --count "${BASE_COMMIT}..HEAD")
VERSION="${BASE}.${HEIGHT}"
sed -i "s/version: \"[^\"]*\"/version: \"${VERSION}\"/" mix.exs
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"

- name: Generate release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api repos/${{ github.repository }}/releases/generate-notes \
-f tag_name="v${{ steps.version.outputs.version }}" \
--jq .body > release_notes.md

- name: Download benchmark results
uses: actions/download-artifact@v4
# Non-blocking: publish proceeds even if the benchmark artifact is absent.
continue-on-error: true
with:
name: benchmark-results

- name: Append benchmarks to release notes
run: |
if [ -f bench_results.txt ]; then
{
echo ""
echo "## Benchmarks"
echo ""
echo "Measured against \`mcr.microsoft.com/mssql/server:2022-latest\` on \`ubuntu-latest\`."
echo ""
echo '```'
cat bench_results.txt
echo '```'
} >> release_notes.md
fi

- name: Publish to Hex.pm
env:
HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
run: mix hex.publish --yes

- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "v${{ steps.version.outputs.version }}" \
--title "v${{ steps.version.outputs.version }}" \
--notes-file release_notes.md
4 changes: 4 additions & 0 deletions .testcontainers.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Disable the Ryuk reaper container.
# Ryuk requires privileged mode and does not work with rootless Podman.
# Containers are still stopped when the test process exits cleanly.
ryuk.disabled=true
43 changes: 0 additions & 43 deletions .travis.yml

This file was deleted.

161 changes: 161 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# AGENTS.md

Guidelines for AI agents and automated tools contributing to **ex_sql_client**.

---

## What this project is

`ex_sql_client` is an Elixir Microsoft SQL Server driver. It is a thin Elixir
wrapper over a .NET worker process (via [Netler](https://github.com/svan-jansson/netler))
that uses `Microsoft.Data.SqlClient` to communicate with SQL Server.

The library requires a running SQL Server instance for its integration tests.

---

## Repository layout

```
lib/
ex_sql_client.ex # Public API – the main entry point
ex_sql_client/
connection.ex # DBConnection behaviour implementation
protocol.ex # Netler RPC call helpers
query.ex # Query struct
result.ex # Result struct
Comment thread
svan-jansson marked this conversation as resolved.
dotnet/dotnet_sql_client/
DotnetSqlClient.csproj # .NET 8 project file
Program.cs # Netler.NET server bootstrap
SqlAdapter.cs # SQL Server operations via Microsoft.Data.SqlClient
test/
query_test.exs
transaction_test.exs
prepared_statement_test.exs
data_type_test.exs
test_helper.exs
mix.exs # Build config and project metadata
docker-compose.yml # Local SQL Server for integration tests
.github/workflows/ # GitHub Actions CI (build + test + publish)
Comment thread
svan-jansson marked this conversation as resolved.
```

---

## Building

Prerequisites: Elixir ≥ 1.9, Erlang/OTP, .NET 8 SDK, running SQL Server instance.

```bash
mix deps.get
mix compile --warnings-as-errors
```

The `--warnings-as-errors` flag is enforced in CI; treat compiler warnings as
bugs.

The Netler compiler (`mix compile.netler`) automatically builds the .NET project
in `dotnet/dotnet_sql_client/` and places the binary in `priv/`.
Comment thread
svan-jansson marked this conversation as resolved.

---

## Testing

Integration tests spin up a SQL Server container automatically via
[Testcontainers](https://hex.pm/packages/testcontainers). Docker (or a
compatible runtime) must be available on the machine.

```bash
mix test --only integration
```

The container is started once in `test/test_helper.exs` and the connection
string is shared with all test modules via `Application.put_env`. All tests are
tagged with `@tag :integration`. There are no unit tests that run without a
live database.

---

## Code conventions

### Elixir

- Format every file with `mix format` before committing. The formatter config
lives in `.formatter.exs`.
- Follow standard Elixir naming: `snake_case` functions, `CamelCase` modules.
- Public functions should have `@doc` and `@spec` annotations.
- Return values use tagged tuples (`{:ok, value}` / `{:error, reason}`). Do not
raise exceptions across module boundaries.
- The Netler route names in `Program.cs` must match the atoms used in the
Elixir protocol layer exactly.

### C\#

- The .NET project targets `net8.0`. Do not downgrade the target framework.
- Use `Microsoft.Data.SqlClient` (not the deprecated `System.Data.SqlClient`).
- Follow existing naming conventions: `PascalCase` for methods and classes,
`camelCase` for locals.
- Route handler methods in `SqlAdapter.cs` must have the signature
`public object MethodName(params object[] parameters)`.

Comment thread
svan-jansson marked this conversation as resolved.
---

## Architecture notes

The call chain for a query is:

```
Elixir caller
→ ExSqlClient (DBConnection behaviour)
→ Netler RPC (TCP socket, port process)
→ Program.cs (Netler.NET server)
→ SqlAdapter.cs
→ Microsoft.Data.SqlClient
→ SQL Server
```

Key invariants:
- The .NET process is started by Netler as a port. It listens on a TCP port
passed as `args[0]`; the Elixir PID is passed as `args[1]`.
- All route names in `Program.cs` must be registered and match what the
Elixir layer calls via Netler.
- Connection and transaction state is held in `SqlAdapter` — one instance per
connection process.

---

## What to work on

Good first contributions:
- Expanding test coverage for edge-case data types.
- Adding `@spec` / `@type` annotations to the Elixir modules.
- Improving error propagation from the .NET layer to Elixir.
- Updating dependencies as new versions are released.

Areas requiring extra care:
- Anything touching `Program.cs` or `SqlAdapter.cs` — changes must compile
with the pinned .NET version and be verified against a live SQL Server.
- Netler version upgrades — the RPC protocol may change between major versions;
always check `Program.cs` against the new Netler.NET API.
- `DBConnection` behaviour callbacks — maintain compatibility with the
`db_connection` contract.

---

## Pull request checklist

- [ ] `mix compile --warnings-as-errors` passes.
- [ ] `mix test --only integration` passes against a local SQL Server.
- [ ] `mix format --check-formatted` passes.
- [ ] `mix credo` passes.
- [ ] New public functions have `@doc` and `@spec`.
- [ ] Commit messages are concise and in the imperative mood.

---

## Out of scope

- Windows CI — the SQL Server Docker container is Linux-only in GitHub Actions;
the library itself is cross-platform but CI runs on Linux.
- Supporting databases other than Microsoft SQL Server.
- Changing the `DBConnection` protocol — maintain compatibility with standard
Elixir database tooling (Ecto, etc.).
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.2
Loading