Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9bfbe09
Add isolated live database CI jobs and migration contract tests
jogibear9988 Sep 22, 2026
19e904d
Complete Db2 and Informix migration operations and correct database t…
jogibear9988 Sep 22, 2026
c07fbb7
Configure native Linux drivers and enforce shell startup failures
jogibear9988 Sep 22, 2026
a53512f
Fix Firebird column removal and retry transient IBM image downloads
jogibear9988 Sep 22, 2026
c913b89
Add public Sybase ASE live coverage and native provider operations
jogibear9988 Sep 22, 2026
d12e0a0
Preserve Db2 schema across migration transactions and enforce key nul…
jogibear9988 Sep 22, 2026
692710d
Fix native readiness and default changes; audit complete test assignment
jogibear9988 Sep 22, 2026
dcdc854
Allocate ASE test storage and use Informix server-only administration
jogibear9988 Sep 22, 2026
7f7ad24
Address metadata review findings with live regression tests and fix A…
jogibear9988 Sep 22, 2026
90bdad4
Use Firebird database API and enable logged ASE column alterations
jogibear9988 Sep 22, 2026
8dd90ac
Preserve inline index flags and ASE constraint-owned index metadata
jogibear9988 Sep 22, 2026
2eca98d
Create flagged column indexes using native standalone statements
jogibear9988 Sep 22, 2026
848800d
Separate ASE data and log allocations for logged schema changes
jogibear9988 Sep 22, 2026
cebced2
Identify and remove Informix constraint-owned indexes correctly
jogibear9988 Sep 22, 2026
bfef305
Place ASE test data and logs on dedicated disposable devices
jogibear9988 Sep 22, 2026
d38670f
Round-trip Firebird defaults and expose MySQL unique constraint metadata
jogibear9988 Sep 22, 2026
3aa4678
Preserve numeric shapes, key membership and typed catalog values
jogibear9988 Sep 22, 2026
bd511f5
Register numeric parameters correctly and bind binary data values
jogibear9988 Sep 22, 2026
a9cdb7e
Fix Informix length and Firebird native type metadata round trips
jogibear9988 Sep 22, 2026
d622aa9
Honor unique column changes and preserve Informix character storage
jogibear9988 Sep 22, 2026
f384c79
Correct live regression fixtures for Informix row limits and ASE exce…
jogibear9988 Sep 22, 2026
5eb07ee
Wait for Informix catalog initialization before live tests
jogibear9988 Sep 22, 2026
1cf71f1
Preserve ASE large-text capacity and decode Informix catalog defaults
jogibear9988 Sep 22, 2026
ca2fa58
Normalize Informix boolean literal wrappers before typed parsing
jogibear9988 Sep 22, 2026
f7e77fb
Remove Informix catalog literal terminators without trimming user text
jogibear9988 Sep 22, 2026
fafe3d4
Match Informix CURRENT defaults to the recreated timestamp precision
jogibear9988 Sep 22, 2026
8c0743b
Verify persisted Informix whitespace independently of driver trimming
jogibear9988 Sep 22, 2026
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.sh text eol=lf
74 changes: 74 additions & 0 deletions .github/scripts/start-database.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
set -euo pipefail
database="$1"
# Registry timeouts are transient; retry downloads, never test failures.
pull() {
for attempt in 1 2 3; do
if docker pull "$1"; then return 0; fi
sleep 5
done
return 1
}
case "$database" in
Unit|SQLite) exit 0 ;;
MySQL)
docker run -d --name migrator-db -p 3306:3306 -e MYSQL_ROOT_PASSWORD=rootpass -e MYSQL_DATABASE=testdb -e MYSQL_USER=testuser -e MYSQL_PASSWORD=testpass mysql:8.0.44
ready() { docker exec migrator-db mysql -uroot -prootpass -e 'SELECT 1' >/dev/null 2>&1; }
;;
MariaDB)
docker run -d --name migrator-db -p 3306:3306 -e MARIADB_ROOT_PASSWORD=rootpass -e MARIADB_DATABASE=testdb mariadb:11.4.10
ready() { docker exec migrator-db mariadb -uroot -prootpass -e 'SELECT 1' >/dev/null 2>&1; }
;;
PostgreSQL)
docker run -d --name migrator-db -p 5432:5432 -e POSTGRES_USER=testuser -e POSTGRES_PASSWORD=testpass postgres:13.23
ready() { docker exec migrator-db pg_isready -U testuser >/dev/null 2>&1; }
;;
SQLServer)
docker run -d --name migrator-db -p 1433:1433 -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD=YourStrong@Passw0rd mcr.microsoft.com/mssql/server:2019-CU32-ubuntu-20.04
ready() { docker exec migrator-db /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P 'YourStrong@Passw0rd' -Q 'SELECT 1' >/dev/null 2>&1; }
;;
Oracle)
docker run -d --name migrator-db -p 1521:1521 -e ORACLE_PASSWORD=adfkweflajdfglkj gvenzl/oracle-free:23.9-slim-faststart
ready() { docker exec migrator-db healthcheck.sh >/dev/null 2>&1; }
;;
Firebird)
docker run -d --name migrator-db -p 3050:3050 -e FIREBIRD_ROOT_PASSWORD=masterkey -e FIREBIRD_DATABASE=test.fdb firebirdsql/firebird:5.0.3
ready() { echo 'select 1 from rdb$database;' | docker exec -i migrator-db isql -b -u SYSDBA -p masterkey localhost:/var/lib/firebird/data/test.fdb >/dev/null 2>&1; }
;;
Db2)
pull icr.io/db2_community/db2:11.5.9.0
docker run -d --name migrator-db --privileged -p 50000:50000 -e LICENSE=accept -e DB2INST1_PASSWORD=testpass -e DBNAME=testdb -e ARCHIVE_LOGS=false -e AUTOCONFIG=false icr.io/db2_community/db2:11.5.9.0
ready() { docker logs migrator-db 2>&1 | grep -q 'Setup has completed'; }
;;
Informix)
pull icr.io/informix/informix-developer-database:15.0.1.0.3
docker run -dt --name migrator-db --hostname ifx --privileged -p 9088:9088 -e LICENSE=accept icr.io/informix/informix-developer-database:15.0.1.0.3
ready() { docker exec migrator-db bash -c 'source /usr/local/bin/informix_inf.env; onstat - >/dev/null; test $? -eq 5' >/dev/null 2>&1; }
;;
Sybase)
pull datagrip/sybase:16.0
docker run -dt --name migrator-db -p 5000:5000 datagrip/sybase:16.0
ready() { printf 'select 12345\ngo\n' | docker exec -i migrator-db bash -c 'source /opt/sybase/SYBASE.sh; isql -Usa -PmyPassword -Slocalhost:5000' 2>/dev/null | grep -q 12345; }
;;
*) echo "Unknown database: $database" >&2; exit 1 ;;
esac
for attempt in $(seq 1 120); do
if ready; then break; fi
if [ "$attempt" -eq 120 ]; then docker logs migrator-db; exit 1; fi
sleep 5
done
case "$database" in
Sybase)
printf "disk init name='migrator_data', physname='/opt/sybase/migrator_data.dat', size='128M'\ngo\ndisk init name='migrator_log', physname='/opt/sybase/migrator_log.dat', size='64M'\ngo\n" | docker exec -i migrator-db bash -c 'source /opt/sybase/SYBASE.sh; isql -b -Usa -PmyPassword -Slocalhost:5000'
;;
SQLServer) docker exec migrator-db /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P 'YourStrong@Passw0rd' -b -Q 'CREATE DATABASE [Whatever];' ;;
Oracle) docker exec -i migrator-db sqlplus -s / as sysdba < .github/workflows/sql/oracle.sql ;;
Informix)
# ONLINE can precede completion of the image's catalog initialization.
for attempt in $(seq 1 24); do
if echo 'create database testdb with log;' | docker exec -i migrator-db bash -c 'source /usr/local/bin/informix_inf.env; dbaccess sysmaster -'; then break; fi
if [ "$attempt" -eq 24 ]; then docker logs migrator-db; exit 1; fi
sleep 5
done
;;
esac
20 changes: 20 additions & 0 deletions .github/scripts/test.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
param(
[ValidateSet('Unit','SQLite','SQLServer','PostgreSQL','Oracle','MySQL','MariaDB','Firebird','Db2','Informix','Sybase')]
[string]$Database = 'Unit'
)
$ErrorActionPreference = 'Stop'
$databases = @('SQLite','SQLServer','PostgreSQL','Oracle','MySQL','MariaDB','Firebird','Db2','Informix','Sybase')
$filter = if ($Database -eq 'Unit') { ($databases | ForEach-Object { "TestCategory!=$_" }) -join '&' } else { "TestCategory=$Database" }
$xmlDirectory = Join-Path (Get-Location) "TestResults/$Database"
dotnet test Migrator.slnx --no-build --filter $filter --logger "trx;LogFileName=$Database.trx" --results-directory TestResults -- NUnit.NumberOfTestWorkers=0 "NUnit.TestOutputXml=$xmlDirectory"
if ($LASTEXITCODE -ne 0) { throw "Tests failed for $Database" }
[xml]$results = Get-Content "TestResults/$Database.trx"
$counters = $results.TestRun.ResultSummary.Counters
if ([int]$counters.executed -eq 0) { throw "No tests executed for $Database" }
if ([int]$counters.failed -gt 0) { throw "Failures in $Database results" }
$skipped = @($results.TestRun.Results.UnitTestResult | Where-Object outcome -eq NotExecuted)
Write-Host "$Database : $($counters.passed) passed, $($skipped.Count) skipped"
foreach ($test in $skipped) { Write-Host "Skipped: $($test.testName) $($test.Output.ErrorInfo.Message)" }
if ($Database -in @('MySQL','MariaDB','Firebird','Db2','Informix','Sybase') -and $skipped.Count -gt 0) {
throw "New database suites must not skip tests."
}
40 changes: 40 additions & 0 deletions .github/scripts/verify-test-coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Ensure database filters form a complete, disjoint partition of NUnit discovery."""
import pathlib
import sys
import xml.etree.ElementTree as ET

expected = {"Unit", "SQLite", "SQLServer", "PostgreSQL", "Oracle", "MySQL",
"MariaDB", "Firebird", "Db2", "Informix", "Sybase"}
seen = {}
executed = 0
counts = set()
found = set()
for path in pathlib.Path(sys.argv[1]).rglob("Migrator.Tests.xml"):
database = path.parent.name
if database not in expected:
raise SystemExit(f"Unexpected result file: {path}")
if database in found:
raise SystemExit(f"Duplicate suite: {database}")
found.add(database)
root = ET.parse(path).getroot()
counts.add(int(root.attrib["testcasecount"]))
tests = root.findall(".//test-case")
if not tests or not any(t.get("result") == "Passed" for t in tests):
raise SystemExit(f"No passing tests: {database}")
executed += len(tests)
for test in tests:
name = test.attrib["fullname"]
# NUnit can discover inherited or repeated cases with identical full names.
# They are valid within one job, but must never appear in another job.
if name in seen and seen[name] != database:
raise SystemExit(f"Test assigned to both {seen[name]} and {database}: {name}")
seen[name] = database
if test.get("result") not in {"Passed", "Skipped"}:
raise SystemExit(f"Test did not pass: {name}")
print(f"{database}: {len(tests)} tests")

if found != expected:
raise SystemExit(f"Missing suites: {sorted(expected - found)}")
if len(counts) != 1 or executed != next(iter(counts)):
raise SystemExit(f"Discovery reports {counts} tests but jobs covered {executed} test cases")
Comment thread
jogibear9988 marked this conversation as resolved.
print(f"All {executed} discovered tests assigned exactly once.")
160 changes: 68 additions & 92 deletions .github/workflows/dotnetpull.yml
Original file line number Diff line number Diff line change
@@ -1,108 +1,84 @@
name: .NET Pull Request

on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: live-databases-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
test:
name: Test (${{ matrix.database }})
runs-on: ubuntu-22.04
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2019-latest
ports:
- 1433:1433
env:
SA_PASSWORD: YourStrong@Passw0rd
ACCEPT_EULA: Y
options: >-
--health-cmd "bash -c '</dev/tcp/localhost/1433' && exit 0 || exit 1"
--health-interval=10s
--health-timeout=5s
--health-retries=10
postgres:
image: postgres:13
ports:
- 5432:5432
env:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
# As of v16 we can use:
# POSTGRES_INITDB_ARGS: "-c max_connections=300"
options: >-
--health-cmd="pg_isready -U testuser"
--health-interval=10s
--health-timeout=5s
--health-retries=5
oracle:
image: gvenzl/oracle-free:latest
ports:
- 1521:1521
env:
ORACLE_PASSWORD: adfkweflajdfglkj
options: >-
--health-cmd healthcheck.sh
--health-interval 10s
--health-timeout 5s
--health-retries 10
mysql:
image: mysql:8.0
ports:
- 3306:3306
env:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: testdb
MYSQL_USER: testuser
MYSQL_PASSWORD: testpass
options: >-
--health-cmd="mysqladmin ping -h localhost -u root -prootpass"
--health-interval=10s
--health-timeout=5s
--health-retries=10
timeout-minutes: 35
strategy:
fail-fast: false
matrix:
database: [Unit, SQLite, SQLServer, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix, Sybase]
steps:
- uses: actions/checkout@v4
- uses: gvenzl/setup-oracle-sqlcl@v1
- name: Setup .NET
uses: actions/setup-dotnet@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: |
9.0.x
- name: Install Microsoft GPG apt-key
dotnet-version: 9.0.x
- name: Start database
shell: bash
run: |
wget https://packages.microsoft.com/keys/microsoft.asc -O microsoft.asc
gpg --dearmor microsoft.asc
chmod 644 microsoft.asc.gpg
sudo mv microsoft.asc.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
- name: Add Microsoft SQL Server repo
run: |
echo "deb [arch=amd64] https://packages.microsoft.com/config/ubuntu/22.04/prod jammy main"
sudo apt-get update
- name: Install SQLCMD tools
run: |
sudo ACCEPT_EULA=Y apt-get install -y mssql-tools unixodbc-dev
echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bashrc
source ~/.bashrc
- name: Create SQLServer database
run: |
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'YourStrong@Passw0rd' -Q "CREATE DATABASE [Whatever];"
- name: Create Oracle user
run: |
sql sys/adfkweflajdfglkj@localhost/FREEPDB1 as sysdba <<EOF
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO ON
SET FEEDBACK ON
SET SERVEROUTPUT ON
@.github/workflows/sql/oracle.sql
EXIT;
EOF

- name: Restore dependencies
run: |
dotnet restore Migrator.slnx
mkdir -p TestResults
bash .github/scripts/start-database.sh "${{ matrix.database }}" 2>&1 | tee TestResults/startup.log
timeout-minutes: 15
- name: Build
run: dotnet build Migrator.slnx -p:LiveDatabase=${{ matrix.database }}
- name: Configure native IBM drivers
if: matrix.database == 'Db2' || matrix.database == 'Informix'
shell: bash
run: |
dotnet build Migrator.slnx
sudo apt-get update
sudo apt-get install -y libaio1 libxml2 unixodbc libncurses5
output="$GITHUB_WORKSPACE/src/Migrator.Tests/bin/Debug/net9.0"
if [ "${{ matrix.database }}" = Db2 ]; then
echo "DB2_CLI_DRIVER_INSTALL_PATH=$output/clidriver" >> "$GITHUB_ENV"
echo "LD_LIBRARY_PATH=$output/clidriver/lib" >> "$GITHUB_ENV"
else
echo "DELIMIDENT=y" >> "$GITHUB_ENV"
echo "INFORMIXDIR=$output/native" >> "$GITHUB_ENV"
echo "LD_LIBRARY_PATH=$output/native/lib:$output/native/lib/cli:$output/native/lib/esql" >> "$GITHUB_ENV"
fi
- name: Test
shell: pwsh
run: ./.github/scripts/test.ps1 -Database ${{ matrix.database }}
Comment thread
jogibear9988 marked this conversation as resolved.
- name: Collect database logs
if: always()
run: |
dotnet test Migrator.slnx
mkdir -p TestResults
if docker inspect migrator-db >/dev/null 2>&1; then
docker logs migrator-db > TestResults/database.log 2>&1
docker inspect migrator-db > TestResults/container.json
fi
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.database }}
path: TestResults/
if-no-files-found: error
- name: Remove test container
if: always()
run: |
if docker inspect migrator-db >/dev/null 2>&1; then
docker rm -fv migrator-db
fi
coverage:
name: Verify complete test coverage
needs: test
runs-on: ubuntu-22.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
pattern: test-results-*
path: TestResults
- run: python3 .github/scripts/verify-test-coverage.py TestResults
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ packages/
/src/GlobalAssemblyInfo.cs
*.gpState

**/appsettings.Development.json
**/appsettings.Development.json
TestResults/
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,7 @@ In this fork the main changes are:

[1]: http://code.google.com/p/migratordotnet/
[2]: https://github.com/migratordotnet/Migrator.NET

## Live database testing

See [live database testing](docs/live-database-tests.md) for the CI matrix, pinned versions, local commands, coverage, engine limitations and excluded candidates.
Loading
Loading