From b65936e2bf23d69b5576583c512d6ce498ae3bcb Mon Sep 17 00:00:00 2001 From: Masaki Ikeda Date: Sat, 17 Oct 2026 20:19:49 +0900 Subject: [PATCH 1/5] Fix CLR DATE formatting overflow before 1753 (#43) Format DATE values without converting them to SqlDateTime. Keep regression coverage focused on CLR TableToString, where the overflow occurred. --- Tests/TableToTextTests.class.sql | 25 +++++++++++++++++++++++++ tSQLtCLR/tSQLtCLR/tSQLtPrivate.cs | 4 ++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Tests/TableToTextTests.class.sql b/Tests/TableToTextTests.class.sql index a84aba4f5..6d3c19bf7 100644 --- a/Tests/TableToTextTests.class.sql +++ b/Tests/TableToTextTests.class.sql @@ -783,6 +783,31 @@ BEGIN END; GO +CREATE PROCEDURE TableToTextTests.[test CLR TableToString works for DATE values across the full supported range] +AS +BEGIN + CREATE TABLE #DoesExist(T DATE); + INSERT INTO #DoesExist (T)VALUES('0001-01-01'); + INSERT INTO #DoesExist (T)VALUES('1752-12-31'); + INSERT INTO #DoesExist (T)VALUES('1753-01-01'); + INSERT INTO #DoesExist (T)VALUES('9999-12-31'); + INSERT INTO #DoesExist (T)VALUES(NULL); + + DECLARE @result NVARCHAR(MAX); + SET @result = tSQLt.Private::TableToString('#DoesExist', 'T', ''); + + DECLARE @expected NVARCHAR(MAX); + SET @expected = '|T | ++----------+ +|!NULL! | +|0001-01-01| +|1752-12-31| +|1753-01-01| +|9999-12-31|'; + EXEC tSQLt.AssertEqualsString @expected, @result; +END; +GO + CREATE PROCEDURE TableToTextTests.[test TableToText works for CLR datatype that is not in tempdb] AS BEGIN diff --git a/tSQLtCLR/tSQLtCLR/tSQLtPrivate.cs b/tSQLtCLR/tSQLtCLR/tSQLtPrivate.cs index 6d18ddfec..ab5387a14 100644 --- a/tSQLtCLR/tSQLtCLR/tSQLtPrivate.cs +++ b/tSQLtCLR/tSQLtCLR/tSQLtPrivate.cs @@ -320,8 +320,8 @@ private static string unquote(string columnName) return columnName.Replace("[","").Replace("]",""); } - private static string SqlDateToString(SqlDateTime dtValue) { - return String.Format("{0:yyyy-MM-dd}", dtValue.Value); + private static string SqlDateToString(DateTime dtValue) { + return String.Format("{0:yyyy-MM-dd}", dtValue); } private static String SqlDateTimeToString(SqlDateTime dtValue) { From aa5b8abf7b785b8ec76c4d1290da61c9c1df8681 Mon Sep 17 00:00:00 2001 From: Masaki Ikeda Date: Sun, 18 Oct 2026 15:31:42 +0900 Subject: [PATCH 2/5] Format CLR DATE values with invariant culture --- tSQLtCLR/tSQLtCLR/tSQLtPrivate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tSQLtCLR/tSQLtCLR/tSQLtPrivate.cs b/tSQLtCLR/tSQLtCLR/tSQLtPrivate.cs index ab5387a14..dd3a9001f 100644 --- a/tSQLtCLR/tSQLtCLR/tSQLtPrivate.cs +++ b/tSQLtCLR/tSQLtCLR/tSQLtPrivate.cs @@ -321,7 +321,7 @@ private static string unquote(string columnName) } private static string SqlDateToString(DateTime dtValue) { - return String.Format("{0:yyyy-MM-dd}", dtValue); + return dtValue.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture); } private static String SqlDateTimeToString(SqlDateTime dtValue) { From 621556564c4d7f45f55bf660f0f6a6ef7d92293c Mon Sep 17 00:00:00 2001 From: Masaki Ikeda Date: Sun, 18 Oct 2026 16:55:49 +0900 Subject: [PATCH 3/5] Add GitHub Actions CI for build and validation on SQL Server (Linux) Run LocalBuild.ps1 and LocalValidate.ps1 against SQL Server 2017, 2019 and 2022 service containers, mirroring the dev container setup with a throwaway self-signed signing key. Test results are summarized on the run page and uploaded as artifacts. Remove build_and_test_on_spawn.yml, which depended on the discontinued Spawn service and on ant build files that no longer exist. --- .github/workflows/build_and_test_on_spawn.yml | 392 ------------------ .github/workflows/ci.yml | 146 +++++++ 2 files changed, 146 insertions(+), 392 deletions(-) delete mode 100644 .github/workflows/build_and_test_on_spawn.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/build_and_test_on_spawn.yml b/.github/workflows/build_and_test_on_spawn.yml deleted file mode 100644 index c87d6fa45..000000000 --- a/.github/workflows/build_and_test_on_spawn.yml +++ /dev/null @@ -1,392 +0,0 @@ -# Builds and tests a database project (in this case tSQLt) -# -# Originally from: https://github.com/lizbaron/db-cicd-with-github-actions/blob/9a514ed0c41029daa231db22e30de7806636160c/.github/workflows/build_and_test_on_linux.yml - -name: Build and Test tSQLt on Spawn - -on: - push: - branches: - - main - workflow_dispatch: - inputs: - spawnDataImages: - description: 'names of the data-images on spawn (e.g. all is currently equivalent to [["SQL2017","mssql-empty:v2017"],["SQL2019","mssql-empty:v2019"]] 2021-09-26)' - required: true - default: 'all' - -env: - MATRIX_NAME: 0 - MATRIX_SPAWNIMAGE: 1 - SPAWNDATAIMAGES_DEFAULT: - '[["SQL2017","mssql-empty:v2017"],["SQL2019","mssql-empty:v2019"]]' - SQLPACKAGE_HOME: 'C:\Program Files\Microsoft SQL Server\160\DAC\bin\' - -jobs: - - set-up-env-vars: - name: set up matrix variables - #if: ${{ always() }} - - # More types found here, https://docs.github.com/en/free-pro-team@latest/actions/reference/specifications-for-github-hosted-runners - runs-on: windows-latest - - env: - TSQLT_ARTIFACT_DIR: ${{ github.workspace }}\tSQLt\Build\output\tSQLt - - steps: - - - name: create variables - id: create-vars - shell: pwsh - run: | - $spawnDataImages = '${{ github.event.inputs.spawnDataImages }}'; - Write-Host "✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ "; - Write-Host ""; - Write-Host ("input spawnDataImages: {0}" -f $spawnDataImages); - if ([string]::IsNullOrEmpty($spawnDataImages) -or ("all" -eq $spawnDataImages)) { - $spawnDataImages = '${{ env.spawndataimages_default }}'; - } - Write-Host ("final spawnDataImages value: {0}" -f $spawnDataImages); - $matrixValues = '{{"data-image":{0} }}' -f $spawnDataImages; - Write-Host ("matrixValues value: {0}" -f $matrixValues); - Write-Host ""; - Write-Host "✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ "; - - Write-Host ('::set-output name=matrixValues::' + $matrixValues); - - outputs: - matrixValues: ${{ steps.create-vars.outputs.matrixValues }} - - -# 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 - build-tSQLt: - name: build tSQLt - #if: ${{ false }} - - runs-on: windows-latest - - env: - PFX_PATH: ${{ github.workspace }}\tSQLt\Build\tSQLtOfficialSigningKey.pfx - ARTIFACT_DIR: ${{ github.workspace }}\artifacts - MSBUILD_HOME: C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin - - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it. - # But this is misleading, because this variable is only really helpful as ${{ github.workspace }} - - name: Checkout tSQLt - uses: actions/checkout@v2 - with: - path: tSQLt - - # documentation: This is just what you need to do... - - name: create and install unofficial pfx file for signing dlls - shell: pwsh - run: | - $superSecurePassword = "ExtraSuperSecurePassword1!"; - $cert = New-SelfSignedCertificate -FriendlyName "tSQLt_testKey" -Type CodeSigningCert -Subject "CN=localhost" -KeyAlgorithm RSA -HashAlgorithm SHA1 -KeyLength 2048 -SmimeCapabilities -CertStoreLocation "cert:/LocalMachine" -Provider "Microsoft Strong Cryptographic Provider" - $pfxFile = Export-PfxCertificate -cert ("Cert:\LocalMachine\My\" + $cert.thumbprint) -FilePath $env:PFX_PATH -Password (ConvertTo-SecureString -String $superSecurePassword -Force -AsPlainText) - &"${{ github.workspace }}\tSQLt\Build\SnInstallPfx" "$env:PFX_PATH" $superSecurePassword tSQLt_OfficialSigningKey - - - name: ant Build\tSQLt.buildCLR.xml --> Build/output/CLRBuild/ - run: ant -buildfile "${{ github.workspace }}\tSQLt\Build\tSQLt.buildCLR.xml" -D"msbuild.path"="$env:MSBUILD_HOME" all - - - name: ant Build/tSQLt.build.xml --> Build/output/tSQLtBuild - run: ant -buildfile "${{ github.workspace }}\tSQLt\Build\tSQLt.build.xml" all -D"commit.id"="${{ github.sha }}" - - - name: copy tSQLt artifacts to ${{ env.artifact_dir }} - shell: pwsh - run: | - New-Item -ItemType Directory -Path "$env:ARTIFACT_DIR" - $basePath = "${{ github.workspace }}\tSQLt\Build\output\tSQLtBuild\"; - $artifactFiles = @("ReadMe.txt","CommitId.txt","CreateBuildLog.sql","GetFriendlySQLServerVersion.sql","tSQLt.tests.zip","tSQLtFacade.zip","tSQLtFiles.zip","tSQLtSnippets(SQLPrompt).zip","Version.txt"); - Get-ChildItem -Path ($basePath + "*") -Include $artifactFiles | Copy-Item -Destination "$env:ARTIFACT_DIR"; - - - name: Archive production artifact - uses: actions/upload-artifact@v2 - with: - if-no-files-found: error - name: tSQLtBuildArtifact - path: "${{ env.artifact_dir }}\\" - retention-days: 5 - - -# 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 - build-facades: - name: build facade dacpacs - needs: [build-tSQLt,set-up-env-vars] - #if: ${{ always() }} - - env: - DACPAC_ARTIFACT_DIR: ${{ github.workspace }}\tSQLt\Build\output\DacpacBuild - - strategy: - matrix: ${{ fromJson(needs.set-up-env-vars.outputs.matrixValues) }} - fail-fast: true - - # More types found here, https://docs.github.com/en/free-pro-team@latest/actions/reference/specifications-for-github-hosted-runners - runs-on: windows-latest - - steps: - - name: Checkout tSQLt - uses: actions/checkout@v2 - with: - path: tSQLt - - - name: load namespace variables into env - id: create-vars - shell: pwsh - run: | - $sqlcmdPath = (Get-Command sqlcmd).Path.TrimEnd("\\SQLCMD.EXE"); - Write-Host ('::set-output name=sqlcmdPath::'+$sqlcmdPath); - - - name: Install spawnctl - shell: pwsh - run: | - echo "Downloading and installing spawnctl..." - Invoke-WebRequest -Uri "https://run.spawn.cc/spawnctl.exe" -UseBasicParsing -OutFile "${{ github.workspace }}\spawnctl.exe" - & "${{ github.workspace }}\spawnctl.exe" version - - - name: Create Container - id: create-container - shell: pwsh - run: | - & "${{ github.workspace }}\spawnctl.exe" get data-images - - $dataImageName = "${{ matrix.data-image[ env.MATRIX_SPAWNIMAGE ] }}"; - - echo "Creating 'tSQLtOnLinux' Spawn data container from image '$dataImageName'"; - $containerName= & "${{ github.workspace }}\spawnctl.exe" create data-container --image $dataImageName --lifetime 30m -q - $containerJSON= & "${{ github.workspace }}\spawnctl.exe" get data-container $containerName -o json - $containerInfo = ConvertFrom-JSON -inputObject $containerJSON; - $containerFQDN=$containerInfo.host+", "+$containerInfo.port; - $containerUser=$containerInfo.user; - $containerPassword=$containerInfo.password; - echo "Successfully created Spawn data container '$containerName'" - - Write-Host ('::set-output name=containerFQDN::'+$containerFQDN); - Write-Host ('::set-output name=containerUser::'+$containerUser); - Write-Host ('::set-output name=containerPassword::'+$containerPassword); - - env: - SPAWNCTL_ACCESS_TOKEN: ${{ secrets.SPAWNCTL_ACCESS_TOKEN }} - - - name: print sql version info - shell: pwsh - run: | - Write-Host "✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ "; - Write-Host ""; - # Write-Host Invoke-Sqlcmd -ConnectionTimeout 600 -Query "SELECT SUSER_NAME() U,SYSDATETIME() T,@@VERSION V;" -ServerInstance "${{ steps.create-container.outputs.containerFQDN }}" -Username "${{ steps.create-container.outputs.containerUser }}" -Password "${{ steps.create-container.outputs.containerPassword }}" -As DataSet - $DS = Invoke-Sqlcmd -ConnectionTimeout 600 -Query "SELECT SUSER_NAME() U,SYSDATETIME() T,@@VERSION V;" -ServerInstance "${{ steps.create-container.outputs.containerFQDN }}" -Username "${{ steps.create-container.outputs.containerUser }}" -Password "${{ steps.create-container.outputs.containerPassword }}" -As DataSet - - $DS.Tables[0].Rows | %{ echo "{ $($_['U']), $($_['T']), $($_['V']) }" } - Write-Host ""; - Write-Host "✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ "; - - - name: download tSQLtBuildArtifact artifact - uses: actions/download-artifact@v4.1.7 - with: - name: tSQLtBuildArtifact - path: "${{ github.workspace }}\\tSQLt\\Build\\output\\tSQLtBuild" - - - name: build tSQLt and Facade Dacpacs --> Build/output/DacpacBuild/ - shell: pwsh - working-directory: ${{ github.workspace }}\tSQLt\ - run: | - .\Build\SetupDacpacBuild.ps1 -ErrorAction Stop - .\Build\FacadeBuildDacpac.ps1 -ErrorAction Stop -ServerName "${{ steps.create-container.outputs.containerFQDN }}" -DatabaseName "tSQLt_dev" -Login "-U ${{ steps.create-container.outputs.containerUser }} -P ${{ steps.create-container.outputs.containerPassword }}" -SqlCmdPath "${{ steps.create-vars.outputs.sqlcmdPath }}" -SqlPackagePath "$env:SQLPACKAGE_HOME" - .\Build\BuildtSQLtDacpac.ps1 -ErrorAction Stop -ServerName "${{ steps.create-container.outputs.containerFQDN }}" -DatabaseName "tSQLt_dev_dacpac_src" -Login "-U ${{ steps.create-container.outputs.containerUser }} -P ${{ steps.create-container.outputs.containerPassword }}" -SqlCmdPath "${{ steps.create-vars.outputs.sqlcmdPath }}" -SqlPackagePath "$env:SQLPACKAGE_HOME" - - - name: create dacpacs artifact - uses: actions/upload-artifact@v2 - with: - if-no-files-found: error - name: dacpacs - path: "${{ env.dacpac_artifact_dir }}\\" - retention-days: 5 - - -# 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 - create-artifact: - name: create artifact - needs: [build-facades] - #if: ${{ always() }} - - # More types found here, https://docs.github.com/en/free-pro-team@latest/actions/reference/specifications-for-github-hosted-runners - runs-on: windows-latest - - env: - TSQLT_PUBLIC_ARTIFACT_DIR: ${{ github.workspace }}\tSQLt\Build\output\tSQLt\public - TSQLT_VALIDATION_ARTIFACT_DIR: ${{ github.workspace }}\tSQLt\Build\output\tSQLt\validation - - steps: - - name: Checkout tSQLt - uses: actions/checkout@v2 - with: - path: tSQLt - - - name: download tSQLtBuildArtifact artifact - uses: actions/download-artifact@v4.1.7 - with: - name: tSQLtBuildArtifact - path: "${{ github.workspace }}\\tSQLt\\Build\\output\\tSQLtBuild" - - - name: download tSQLt dacpac artifact(s) - uses: actions/download-artifact@v4.1.7 - with: - name: dacpacs - path: "${{ github.workspace }}\\tSQLt\\Build\\output\\DacpacBuild" - - - name: inspect CreateArtifact directory - shell: pwsh - working-directory: ${{ github.workspace }}\tSQLt\Build\output - run: | - Get-ChildItem . -Recurse - - - name: package Build/output/tSQLt /public and /validation files - shell: pwsh - working-directory: ${{ github.workspace }}\tSQLt\ - run: | - .\Build\BuildtSQLtZip.ps1 -ErrorAction Stop - - - name: create tSQLt public artifact - uses: actions/upload-artifact@v2 - with: - if-no-files-found: error - name: tSQLtPublic - path: "${{ env.tsqlt_public_artifact_dir }}\\" - retention-days: 90 - - - name: create tSQLt validation artifact - uses: actions/upload-artifact@v2 - with: - if-no-files-found: error - name: tSQLtValidation - path: "${{ env.tsqlt_validation_artifact_dir }}\\" - retention-days: 5 - -# 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 - test-tSQLt-and-facade: - name: test build artifact - needs: [create-artifact,set-up-env-vars] - #if: ${{ always() }} - - env: - TEST_ARTIFACT_DIR: ${{ github.workspace }}\tSQLt\Build\temp\Validate\TestResults - DACPAC_ARTIFACT_DIR: ${{ github.workspace }}\tSQLt\Build\output\DacpacBuild - MATRIX_VERSIONS: ${{ format('{{"data-image":{0} }}',github.event.inputs.spawnDataImages) }} - - strategy: - matrix: ${{ fromJson(needs.set-up-env-vars.outputs.matrixValues) }} - fail-fast: false - - # More types found here, https://docs.github.com/en/free-pro-team@latest/actions/reference/specifications-for-github-hosted-runners - runs-on: windows-latest - - steps: - - name: Checkout tSQLt - uses: actions/checkout@v2 - with: - path: tSQLt - - - name: download public tSQLt artifact - uses: actions/download-artifact@v4.1.7 - with: - name: tSQLtPublic - path: "${{ github.workspace }}\\tSQLt\\Build\\output\\tSQLt\\public" - - - name: download validation tSQLt artifact - uses: actions/download-artifact@v4.1.7 - with: - name: tSQLtValidation - path: "${{ github.workspace }}\\tSQLt\\Build\\output\\tSQLt\\validation" - - - name: load namespace variables into env - id: create-vars - shell: pwsh - run: | - $sqlcmdPath = (Get-Command sqlcmd).Path.TrimEnd("\\SQLCMD.EXE"); - Write-Host ('::set-output name=sqlcmdPath::'+$sqlcmdPath); - - - name: Install spawnctl - shell: pwsh - run: | - echo "Downloading and installing spawnctl..." - Invoke-WebRequest -Uri "https://run.spawn.cc/spawnctl.exe" -UseBasicParsing -OutFile "${{ github.workspace }}\spawnctl.exe" - & "${{ github.workspace }}\spawnctl.exe" version - - - name: Create Container - id: create-container - shell: pwsh - run: | - & "${{ github.workspace }}\spawnctl.exe" get data-images - - $dataImageName = "${{ matrix.data-image[ env.MATRIX_SPAWNIMAGE] }}"; - - echo "Creating 'tSQLtOnLinux' Spawn data container from image '$dataImageName'"; - $containerName= & "${{ github.workspace }}\spawnctl.exe" create data-container --image $dataImageName --lifetime 30m -q - $containerJSON= & "${{ github.workspace }}\spawnctl.exe" get data-container $containerName -o json - $containerInfo = ConvertFrom-JSON -inputObject $containerJSON; - $containerFQDN=$containerInfo.host+", "+$containerInfo.port; - $containerUser=$containerInfo.user; - $containerPassword=$containerInfo.password; - echo "Successfully created Spawn data container '$containerName'" - - Write-Host ('::set-output name=containerFQDN::'+$containerFQDN); - Write-Host ('::set-output name=containerUser::'+$containerUser); - Write-Host ('::set-output name=containerPassword::'+$containerPassword); - - env: - SPAWNCTL_ACCESS_TOKEN: ${{ secrets.SPAWNCTL_ACCESS_TOKEN }} - - - name: print sql version info - shell: pwsh - run: | - Write-Host "✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ "; - Write-Host ""; - # Write-Host Invoke-Sqlcmd -ConnectionTimeout 600 -Query "SELECT SUSER_NAME() U,SYSDATETIME() T,@@VERSION V;" -ServerInstance "${{ steps.create-container.outputs.containerFQDN }}" -Username "${{ steps.create-container.outputs.containerUser }}" -Password "${{ steps.create-container.outputs.containerPassword }}" -As DataSet - $DS = Invoke-Sqlcmd -ConnectionTimeout 600 -Query "SELECT SUSER_NAME() U,SYSDATETIME() T,@@VERSION V;" -ServerInstance "${{ steps.create-container.outputs.containerFQDN }}" -Username "${{ steps.create-container.outputs.containerUser }}" -Password "${{ steps.create-container.outputs.containerPassword }}" -As DataSet - - $DS.Tables[0].Rows | %{ echo "{ $($_['U']), $($_['T']), $($_['V']) }" } - Write-Host ""; - Write-Host "✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ ✨ "; - - - name: Setup SQL Users - shell: pwsh - run: | - $DS = Invoke-Sqlcmd -ConnectionTimeout 600 -InputFile "${{ github.workspace }}\tSQLt\Build\CreateLogin(tSQLt.Build).sql" -ServerInstance "${{ steps.create-container.outputs.containerFQDN }}" -Username "${{ steps.create-container.outputs.containerUser }}" -Password "${{ steps.create-container.outputs.containerPassword }}" - - # @SM --> TODO: jUnit results - - name: ant Build/tSQLt.validatebuild.xml - working-directory: ${{ github.workspace }}\tSQLt\Build - run: ant -buildfile tSQLt.validatebuild.xml all -D"db.server"="${{ steps.create-container.outputs.containerFQDN }}" -D"db.name"=tSQLt_dev -D"db.login"="-U ${{ steps.create-container.outputs.containerUser }} -P ${{ steps.create-container.outputs.containerPassword }}" -D"sqlcmd.path"="${{ steps.create-vars.outputs.sqlcmdPath }}" -D"sqlpackage.path"="$env:SQLPACKAGE_HOME" -D"logtable.name"="tempdb.dbo.[Full Build Log]" - - - name: create junit artifact - shell: pwsh - run: | - $ArtifactPath = "$env:TEST_ARTIFACT_DIR\"; - (Get-ChildItem $basePath -recurse -include "TestResults*.xml").FullName; - - - name: archive junit test artifact - uses: actions/upload-artifact@v2 - with: - if-no-files-found: error - name: jUnitTests-${{ matrix.data-image[ env.MATRIX_NAME ] }} - path: "${{ env.test_artifact_dir }}\\" - retention-days: 90 - - -# # 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 💖 -# drop-data-container: -# name: drop the data container -# needs: test-tSQLt -# if: ${{ always() }} -# runs-on: ubuntu-latest -# env: -# steps: -# - name: decode az sp cred -# id: cred-decode -# shell: pwsh -# run: | -# Write-Host ('#TODO'); -# # documentation: https://github.com/azure/login#configure-azure-credentials diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..155d55d83 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,146 @@ +# Builds tSQLt and runs the full validation suite against SQL Server on Linux. +# +# Mirrors the dev container setup (.devcontainer/): the CLR assemblies are signed +# with a throwaway self-signed key, so the artifacts are for validation only and +# must not be released. + +name: CI + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build-and-test: + name: build and test (SQL Server ${{ matrix.sqlserver }}) + runs-on: ubuntu-24.04 + timeout-minutes: 90 + + strategy: + fail-fast: false + matrix: + sqlserver: ['2017', '2019', '2022'] + + services: + sqlserver: + image: mcr.microsoft.com/mssql/server:${{ matrix.sqlserver }}-latest + ports: + - 1433:1433 + env: + ACCEPT_EULA: 'Y' + # Must match SA_PASSWORD below (the env context is not available to services). + MSSQL_SA_PASSWORD: 'P@ssw0rd' + + env: + # Ephemeral credentials for the service container and the self-signed signing key. + SA_PASSWORD: 'P@ssw0rd' + PFX_PASSWORD: 'P@ssw0rdP@ssw0rdP@ssw0rdP@ssw0rd' + SQL_SERVER: 'localhost,1433' + + steps: + - name: Checkout tSQLt + uses: actions/checkout@v4 + + - name: Install SqlServer PowerShell module + shell: pwsh + run: | + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + Install-Module -Name SqlServer -Scope CurrentUser -AllowClobber -Force + Get-Module -ListAvailable SqlServer | Select-Object Name, Version | Format-Table + + - name: Install sqlpackage + shell: bash + run: | + curl -sSL https://aka.ms/sqlpackage-linux -o "$RUNNER_TEMP/sqlpackage.zip" + mkdir -p "$RUNNER_TEMP/sqlpackage" + unzip -q "$RUNNER_TEMP/sqlpackage.zip" -d "$RUNNER_TEMP/sqlpackage" + chmod +x "$RUNNER_TEMP/sqlpackage/sqlpackage" + echo "$RUNNER_TEMP/sqlpackage" >> "$GITHUB_PATH" + + - name: Create self-signed signing key + shell: bash + run: | + CERT_DIR="$RUNNER_TEMP/tSQLtCert" + mkdir -p "$CERT_DIR" + cd "$CERT_DIR" + openssl req -x509 -newkey rsa:4096 -keyout tSQLtOfficialSigningKey.key -out tSQLtOfficialSigningKey.crt -days 1 -nodes -subj "/CN=tSQLt-CI" + openssl pkcs12 -export -out tSQLtOfficialSigningKey.pfx -inkey tSQLtOfficialSigningKey.key -in tSQLtOfficialSigningKey.crt -passout pass:"$PFX_PASSWORD" + echo "PFX_PATH=$CERT_DIR/tSQLtOfficialSigningKey.pfx" >> "$GITHUB_ENV" + + - name: Wait for SQL Server + shell: pwsh + run: | + $connectionString = "Server=$env:SQL_SERVER;User Id=sa;Password=$env:SA_PASSWORD;TrustServerCertificate=true;Connect Timeout=5;" + for ($i = 1; $i -le 60; $i++) { + try { + Invoke-Sqlcmd -ConnectionString $connectionString -Query "SELECT @@VERSION AS V;" -ErrorAction Stop | Format-List + exit 0 + } catch { + Write-Host "SQL Server not ready yet (attempt $i): $($_.Exception.Message)" + Start-Sleep -Seconds 5 + } + } + throw "SQL Server did not become ready in time." + + - name: Build + shell: pwsh + run: | + ./Build/LocalBuild.ps1 ` + -ServerName $env:SQL_SERVER ` + -UserName sa ` + -Password (ConvertTo-SecureString $env:SA_PASSWORD -AsPlainText -Force) ` + -pfxFilePath $env:PFX_PATH ` + -pfxPassword (ConvertTo-SecureString $env:PFX_PASSWORD -AsPlainText -Force) + + - name: Validate + shell: pwsh + run: | + ./Build/LocalValidate.ps1 ` + -ServerName $env:SQL_SERVER ` + -UserName sa ` + -Password (ConvertTo-SecureString $env:SA_PASSWORD -AsPlainText -Force) + + - name: Summarize test results + if: always() + shell: pwsh + run: | + $resultFiles = Get-ChildItem -Path ./Build/temp/Validate/Results -Filter 'TestResults*.xml' -Recurse -ErrorAction SilentlyContinue + $summary = @("### tSQLt test results (SQL Server ${{ matrix.sqlserver }})", "", "| Suite | Tests | Failures | Errors | Skipped |", "|---|---:|---:|---:|---:|") + foreach ($file in $resultFiles) { + [xml]$xml = Get-Content -Path $file.FullName -Raw + $suites = @($xml.SelectNodes('//testsuite')) + $sum = { param($attribute) $total = 0; foreach ($s in $suites) { $value = $s.GetAttribute($attribute); if ($value) { $total += [int]$value } }; $total } + $summary += "| $($file.BaseName) | $(& $sum 'tests') | $(& $sum 'failures') | $(& $sum 'errors') | $(& $sum 'skipped') |" + } + if (-not $resultFiles) { $summary += "| (no result files) | | | | |" } + $summary -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-sql${{ matrix.sqlserver }} + path: | + Build/temp/Validate/Results/ + Build/output/Validate/ + if-no-files-found: warn + retention-days: 30 + + - name: Upload build artifact + if: success() + uses: actions/upload-artifact@v4 + with: + name: tSQLt-sql${{ matrix.sqlserver }} + path: Build/output/tSQLt/ + if-no-files-found: error + retention-days: 14 From 7dc69a575f46815b3e036b5ca7da029b95d3979d Mon Sep 17 00:00:00 2001 From: Masaki Ikeda Date: Sun, 18 Oct 2026 18:22:05 +0900 Subject: [PATCH 4/5] Prepare CI for develop integration and preserve validation logs --- .github/workflows/ci.yml | 44 +++++++++++++++++++++++++++++++--------- README.md | 28 +++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 155d55d83..0c6439730 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,12 @@ name: CI on: push: branches: + - develop - main pull_request: + branches: + - develop + - main workflow_dispatch: concurrency: @@ -95,20 +99,30 @@ jobs: - name: Build shell: pwsh run: | - ./Build/LocalBuild.ps1 ` - -ServerName $env:SQL_SERVER ` - -UserName sa ` - -Password (ConvertTo-SecureString $env:SA_PASSWORD -AsPlainText -Force) ` - -pfxFilePath $env:PFX_PATH ` - -pfxPassword (ConvertTo-SecureString $env:PFX_PASSWORD -AsPlainText -Force) + Start-Transcript -Path "$env:RUNNER_TEMP/tSQLt-logs/build.log" -Force + try { + ./Build/LocalBuild.ps1 ` + -ServerName $env:SQL_SERVER ` + -UserName sa ` + -Password (ConvertTo-SecureString $env:SA_PASSWORD -AsPlainText -Force) ` + -pfxFilePath $env:PFX_PATH ` + -pfxPassword (ConvertTo-SecureString $env:PFX_PASSWORD -AsPlainText -Force) + } finally { + Stop-Transcript + } - name: Validate shell: pwsh run: | - ./Build/LocalValidate.ps1 ` - -ServerName $env:SQL_SERVER ` - -UserName sa ` - -Password (ConvertTo-SecureString $env:SA_PASSWORD -AsPlainText -Force) + Start-Transcript -Path "$env:RUNNER_TEMP/tSQLt-logs/validate.log" -Force + try { + ./Build/LocalValidate.ps1 ` + -ServerName $env:SQL_SERVER ` + -UserName sa ` + -Password (ConvertTo-SecureString $env:SA_PASSWORD -AsPlainText -Force) + } finally { + Stop-Transcript + } - name: Summarize test results if: always() @@ -116,14 +130,23 @@ jobs: run: | $resultFiles = Get-ChildItem -Path ./Build/temp/Validate/Results -Filter 'TestResults*.xml' -Recurse -ErrorAction SilentlyContinue $summary = @("### tSQLt test results (SQL Server ${{ matrix.sqlserver }})", "", "| Suite | Tests | Failures | Errors | Skipped |", "|---|---:|---:|---:|---:|") + $totalTests = 0 + $totalFailures = 0 + $totalErrors = 0 foreach ($file in $resultFiles) { [xml]$xml = Get-Content -Path $file.FullName -Raw $suites = @($xml.SelectNodes('//testsuite')) $sum = { param($attribute) $total = 0; foreach ($s in $suites) { $value = $s.GetAttribute($attribute); if ($value) { $total += [int]$value } }; $total } $summary += "| $($file.BaseName) | $(& $sum 'tests') | $(& $sum 'failures') | $(& $sum 'errors') | $(& $sum 'skipped') |" + $totalTests += & $sum 'tests' + $totalFailures += & $sum 'failures' + $totalErrors += & $sum 'errors' } if (-not $resultFiles) { $summary += "| (no result files) | | | | |" } $summary -join "`n" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + if ($totalTests -eq 0 -or $totalFailures -gt 0 -or $totalErrors -gt 0) { + throw "Validation did not pass: $totalTests tests, $totalFailures failures, $totalErrors errors. See the test results and logs." + } - name: Upload test results if: always() @@ -133,6 +156,7 @@ jobs: path: | Build/temp/Validate/Results/ Build/output/Validate/ + ${{ runner.temp }}/tSQLt-logs/ if-no-files-found: warn retention-days: 30 diff --git a/README.md b/README.md index cd477cb9f..a5cbba295 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,32 @@ tSQLt allows you to implement unit tests in T-SQL. This is important as you do n tSQLt is licensed under Apache 2.0 (https://github.com/tSQLt-org/tSQLt/blob/master/Build/License.txt) +## Development in the masmgr fork + +`main` tracks the upstream tSQLt repository. `develop` integrates changes maintained +in this fork. Open pull requests for bug fixes and enhancements against `develop`; +keep each change in its own branch and validate it with CI before merging. + +## Continuous integration + +The GitHub Actions `CI` workflow runs on pushes to `develop` and `main`, pull +requests targeting either branch, and manual dispatch. It builds tSQLt with +`Build/LocalBuild.ps1` and runs the existing validation suite with +`Build/LocalValidate.ps1` against SQL Server 2017, 2019, and 2022 on Linux using +Ubuntu runners. No external service credentials or repository secrets are needed. + +Open the PR checks or the repository's **Actions > CI** page to view each SQL +Server job. The run summary lists test counts, failures, errors, and skipped tests. +Validation fails on test failures or errors, missing expected result files, or an +empty test run. + +Each job uploads `test-results-sql` containing the result XML files, +validation output, and build/validation logs, including available files when a +step fails. These artifacts are retained for 30 days. Successful jobs also upload +`tSQLt-sql` build artifacts, retained for 14 days. + +CI uses a temporary self-signed key. Its build artifacts are for validation only +and must not be distributed as releases. This workflow covers Linux SQL Server; +it does not establish compatibility with Windows or other SQL Server versions. + From 160302d86a00e8d6b2463335ea5b72e04aebe26f Mon Sep 17 00:00:00 2001 From: Masaki Ikeda Date: Sun, 18 Oct 2026 18:40:31 +0900 Subject: [PATCH 5/5] Cover pre-1753 DATE differences through AssertEqualsTable (#43) --- Tests/AssertEqualsTableTests.class.sql | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Tests/AssertEqualsTableTests.class.sql b/Tests/AssertEqualsTableTests.class.sql index 08fb5b046..8da70c478 100644 --- a/Tests/AssertEqualsTableTests.class.sql +++ b/Tests/AssertEqualsTableTests.class.sql @@ -2,6 +2,26 @@ GO EXEC tSQLt.NewTestClass 'AssertEqualsTableTests'; GO +CREATE PROCEDURE AssertEqualsTableTests.[test DATE values before 1753 report assertion differences without CLR overflow] +AS +BEGIN + CREATE TABLE #Expected(DateColumn DATE); + CREATE TABLE #Actual(DateColumn DATE); + INSERT INTO #Expected VALUES ('0001-01-01'), ('1752-12-31'); + INSERT INTO #Actual VALUES ('2000-01-01'); + + DECLARE @FailMessage NVARCHAR(MAX); + EXEC tSQLt_testutil.CaptureFailMessage + @Command = N'EXEC tSQLt.AssertEqualsTable N''#Expected'', N''#Actual'';', + @FailMessage = @FailMessage OUTPUT; + + EXEC tSQLt.AssertLike N'%Unexpected/missing resultset rows!%', @FailMessage; + EXEC tSQLt.AssertLike N'%0001-01-01%', @FailMessage; + EXEC tSQLt.AssertLike N'%1752-12-31%', @FailMessage; + EXEC tSQLt.AssertLike N'%2000-01-01%', @FailMessage; +END; +GO + CREATE PROCEDURE AssertEqualsTableTests.[test left table doesn't exist results in failure] AS BEGIN