diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 9a4c7685..7d766208 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,20 +8,20 @@ assignees: '' --- **Describe the bug** -A clear and concise description of what the bug is. + **To Reproduce** -Steps to reproduce the behavior + **Expected behavior** -A clear and concise description of what you expected to happen. + **Screenshots** -If applicable, add screenshots to help explain your problem. + **Desktop (please complete the following information):** - OS: [e.g. macOS, Linux] - - Version [e.g. 1.0.2] + - Version: [e.g. 1.0.2] **Additional context** -Add any other context about the problem here. + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7d..a6f653e0 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -8,13 +8,13 @@ assignees: '' --- **Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + **Describe the solution you'd like** -A clear and concise description of what you want to happen. + **Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. + **Additional context** -Add any other context or screenshots about the feature request here. + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b67b84b..a103c3da 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,22 +20,21 @@ jobs: #is-release: 'true' steps: - - name: Setup .NET 6 - uses: actions/setup-dotnet@v2 + - name: Setup .NET 7 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x - include-prerelease: false + dotnet-version: 7.0.x - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.9.13 + uses: gittools/actions/gitversion/setup@v0.10.2 with: versionSpec: '5.x' - name: Determine Version id: gitversion - uses: gittools/actions/gitversion/execute@v0.9.13 + uses: gittools/actions/gitversion/execute@v0.10.2 build-netcore-tool: needs: set-version-number @@ -45,11 +44,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Setup .NET 6 - uses: actions/setup-dotnet@v2 + - name: Setup .NET 7 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x - include-prerelease: false + dotnet-version: 7.0.x - name: Restore dependencies run: dotnet restore - name: Build @@ -78,23 +76,22 @@ jobs: matrix: arch: [ "win-x64", "win-x86", "win-arm", "win-arm64", "alpine-x64", "linux-x64", "linux-arm", "linux-arm64", - "osx-x64" + "osx-x64", "osx.11.0-x64", "osx.11.0-arm64", "osx.12-x64", "osx.12-arm64", "osx.13-x64", "osx.13-arm64" ] steps: - uses: actions/checkout@v3 - - name: Setup .NET 6 - uses: actions/setup-dotnet@v2 + - name: Setup .NET 7 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x - include-prerelease: false + dotnet-version: 7.0.x - name: Publish self-contained ${{ matrix.arch }} run: dotnet publish ./grate/grate.csproj -r ${{ matrix.arch }} -c release --self-contained -p:SelfContained=true -o ./publish/${{ matrix.arch }}/self-contained env: VERSION: ${{ needs.set-version-number.outputs.nuGetVersion }} - - name: Publish .NET 6 dependent ${{ matrix.arch }} + - name: Publish .NET 6/7 dependent ${{ matrix.arch }} run: dotnet publish ./grate/grate.csproj -r ${{ matrix.arch }} -c release --no-self-contained -o ./publish/${{ matrix.arch }}/dependent env: VERSION: ${{ needs.set-version-number.outputs.nuGetVersion }} @@ -122,7 +119,7 @@ jobs: if: ${{ needs.set-version-number.outputs.is-release == 'true' }} strategy: matrix: - arch: [ "win-x64" ] + arch: [ "win-x64", "win-arm64" ] steps: - uses: actions/checkout@v3 @@ -164,7 +161,7 @@ jobs: - name: Log in to the Container registry - uses: docker/login-action@49ed152c8eca782a232dede0303416e8f356c37b + uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a with: #registry: ${{ env.REGISTRY }} username: ${{ github.repository_owner}} @@ -172,7 +169,7 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@69f6fc9d46f2f8bf0d5491e4aabe0bb8c6a4678a + uses: docker/metadata-action@c4ee3adeed93b1fa6a762f209fb01608c1a22f1e with: tags: | type=semver,pattern={{version}} @@ -184,7 +181,7 @@ jobs: - name: Build and push Docker image - uses: docker/build-push-action@c84f38281176d4c9cdb1626ffafcd6b3911b5d94 + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 with: context: ./installers/docker/ push: true @@ -227,31 +224,31 @@ jobs: name: grate_${{ needs.set-version-number.outputs.nuGetVersion }}-1_${{ steps.get-arch.outputs.arch}}.deb path: ./installers/deb/grate_${{ needs.set-version-number.outputs.nuGetVersion }}-1_${{ steps.get-arch.outputs.arch }}.deb - build-winget: - name: Winget - Update package manifest in the OWC - needs: - - set-version-number - - build-msi - runs-on: windows-latest - if: ${{ needs.set-version-number.outputs.is-release == 'true' }} + # build-winget: + # name: Winget - Update package manifest in the OWC + # needs: + # - set-version-number + # - build-msi + # runs-on: windows-latest + # if: ${{ needs.set-version-number.outputs.is-release == 'true' }} - steps: - - name: Winget-Create - run: | + # steps: + # - name: Winget-Create + # run: | - $version = "$($env:version)" + # $version = "$($env:version)" - # Download wingetcreate - iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe + # # Download wingetcreate + # iwr https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe - $packageUrl="https://github.com/erikbra/grate/releases/download/$version/grate-$version.msi" + # $packageUrl="https://github.com/erikbra/grate/releases/download/$version/grate-$version.msi" - echo "Running ./wingetcreate.exe update erikbra.grate -u $packageUrl -v $version -t `"$env:WINGET_GH_PAT`" --submit" - ./wingetcreate.exe update erikbra.grate -u $packageUrl -v $version -t "$env:WINGET_GH_PAT" --submit - env: - WINGET_GH_PAT: ${{ secrets.WINGET_GH_PAT }} - #version: "1.4.0" - version: "${{ needs.set-version-number.outputs.nuGetVersion }}" + # echo "Running ./wingetcreate.exe update erikbra.grate -u $packageUrl -v $version -t `"$env:WINGET_GH_PAT`" --submit" + # ./wingetcreate.exe update erikbra.grate -u $packageUrl -v $version -t "$env:WINGET_GH_PAT" --submit + # env: + # WINGET_GH_PAT: ${{ secrets.WINGET_GH_PAT }} + # #version: "1.4.0" + # version: "${{ needs.set-version-number.outputs.nuGetVersion }}" test: @@ -264,13 +261,12 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Setup .NET 6 - uses: actions/setup-dotnet@v2 + - name: Setup .NET 7 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x - include-prerelease: false + dotnet-version: 7.0.x - name: Test - run: dotnet test --filter FullyQualifiedName~grate.unittests.${{ matrix.category }} -c Release --logger:"trx;LogFilePath=test-results-${{ matrix.category }}.xml" + run: dotnet test --filter "FullyQualifiedName~grate.unittests.${{ matrix.category }}" -c Release --logger:"trx;LogFilePath=test-results-${{ matrix.category }}.xml" -- -MaxCpuCount 2 # run: dotnet test --verbosity Normal -c Release --logger "trx;LogFileName=/tmp/test-results/grate.unittests.trx" env: LogLevel: Warning diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 450bbd69..8c193599 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,11 +21,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Setup .NET 6 - uses: actions/setup-dotnet@v2 + - name: Setup .NET 7 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x - include-prerelease: false + dotnet-version: 7.0.x - name: Restore dependencies run: dotnet restore -r linux-x64 grate.unittests/grate.unittests.csproj - name: Build @@ -52,17 +51,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - - name: Setup .NET 6 - uses: actions/setup-dotnet@v2 + - name: Setup .NET 7 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x - include-prerelease: false + dotnet-version: 7.0.x # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 - with: - languages: 'csharp' - name: Autobuild uses: github/codeql-action/autobuild@v2 @@ -86,13 +82,12 @@ jobs: uses: actions/download-artifact@v3 with: name: binaries - - name: Setup .NET 6 - uses: actions/setup-dotnet@v2 + - name: Setup .NET 7 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x - include-prerelease: false + dotnet-version: 7.0.x - name: Test - run: dotnet vstest --TestCaseFilter:"FullyQualifiedName~grate.unittests.${{ matrix.category }}" bin/grate.unittests.dll --logger:"trx;LogFileName=test-results-${{ matrix.category }}.xml" + run: dotnet test --filter "FullyQualifiedName~grate.unittests.${{ matrix.category }}" bin/grate.unittests.dll --logger:"trx;LogFileName=test-results-${{ matrix.category }}.xml" -- -MaxCpuCount 2 env: LogLevel: Warning TZ: UTC diff --git a/.github/workflows/grate-workflow.yml b/.github/workflows/grate-workflow.yml index 8a9d0602..5dbad0f7 100644 --- a/.github/workflows/grate-workflow.yml +++ b/.github/workflows/grate-workflow.yml @@ -18,10 +18,10 @@ jobs: - uses: actions/checkout@v3 - name: Download grate - run: curl -sL https://github.com/erikbra/grate/releases/download/1.0.0/grate_1.0.0-1_amd64.deb -o /tmp/grate_1.0.0-1_amd64.deb + run: curl -sL https://github.com/erikbra/grate/releases/download/1.4.0/grate_1.4.0-1_amd64.deb -o /tmp/grate_1.4.0-1_amd64.deb - name: Install grate - run: sudo dpkg -i /tmp/grate_1.0.0-1_amd64.deb + run: sudo dpkg -i /tmp/grate_1.4.0-1_amd64.deb - name: Verify grate installation run: grate --help @@ -43,10 +43,10 @@ jobs: - uses: actions/checkout@v3 - name: Download grate - run: curl -sL https://github.com/erikbra/grate/releases/download/1.0.0/grate_1.0.0-1_amd64.deb -o /tmp/grate_1.0.0-1_amd64.deb + run: curl -sL https://github.com/erikbra/grate/releases/download/1.4.0/grate_1.4.0-1_amd64.deb -o /tmp/grate_1.4.0-1_amd64.deb - name: Install grate - run: sudo dpkg -i /tmp/grate_1.0.0-1_amd64.deb + run: sudo dpkg -i /tmp/grate_1.4.0-1_amd64.deb - name: Verify grate installation run: grate --help diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e199189e..a6167e95 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ git clone https://github.com/erikbra/grate.git ``` > cd grate -> dotnet test +> dotnet test --framework net7.0 ``` ## Build a self-contained executable (if you want) diff --git a/docs/ConfigurationOptions/FolderConfiguration.md b/docs/ConfigurationOptions/FolderConfiguration.md index 38c1d76b..07f0644d 100644 --- a/docs/ConfigurationOptions/FolderConfiguration.md +++ b/docs/ConfigurationOptions/FolderConfiguration.md @@ -16,11 +16,11 @@ which might be a good starting point if you have no special requirements. grate works with three different folder types: -| Folder/script type | Explanation | More info | -| ------ | ------- |------- | -| One-time scripts | These are scripts that are run **exactly once** per database, and never again. | [One time scripts](../ScriptTypes/OneTimeScripts.md#one-time-scripts) | -| Anytime Scripts | These scripts are run **any time they're changed** | [Anytime scripts](../ScriptTypes/AnytimeScripts.md#anytime-scripts) | -| Everytime Scripts | These scripts are run (you guessed it) **every time** grate executes :) | [Everytime scripts](../ScriptTypes/EverytimeScripts.md#everytime-scripts) | +| Folder/script type | Name | Explanation | More info | +| ------ |----| ------- |------- | +| One-time scripts | Once | These are scripts that are run **exactly once** per database, and never again. | [One time scripts](../ScriptTypes/OneTimeScripts.md#one-time-scripts) | +| Anytime Scripts | AnyTime | These scripts are run **any time they're changed** | [Anytime scripts](../ScriptTypes/AnytimeScripts.md#anytime-scripts) | +| Everytime Scripts | EveryTime | These scripts are run (you guessed it) **every time** grate executes :) | [Everytime scripts](../ScriptTypes/EverytimeScripts.md#everytime-scripts) | ## Specifying a custom folder configuration @@ -44,9 +44,12 @@ This would use the [default folder configuration](#default-folder-configuration) `ddl` folder for **up** scripts, in the `projections` folder for **views**, and in the `preparefordeploy` folder for **beforemigration** scripts. ``` ---folders up=ddl;views=projections;beforemigration=preparefordeploy +--folders 'up=ddl;views=projections;beforemigration=preparefordeploy' ``` +**NOTE:** Be sure to use quotes when specifying multiple folders in the argument, as many shells treat `;` as +a the "end this command" character, so everything after the `;` will not be part of the command line. + or ``` @@ -81,7 +84,7 @@ The properties you can set per folder, are: | ------ | ------- | ------- | ------- | | Name | the key/name you wish to give to the folder | (doesn't matter if path is specified) | _(none)_ | | Path | the relative path of the folder, relative to the --sqlfilesdirectory parameter. | Any relative path | the **Name** specified above. -| Type | the type of the migration | Once, EveryTime, Anytime | Once | +| Type | the type of the migration | Once, EveryTime, AnyTime | Once | | ConnectionType | whether to run on the default connection, or on the admin | Default, Admin | Default | | TransactionHandling | whether to be part of the transaction (if running the migration in a transaction), or run the script in an autonomous transaction, so that it is always run, even on a rollback | Default, Autonomous | Default | @@ -96,7 +99,7 @@ Example: or ``` ---folders folder1=Once;folder2=Everytime;folder3=Anytime +--folders folder1=Once;folder2=EveryTime;folder3=AnyTime ``` the last one will expect the folders to be named `folder1`, `folder2`, and `folder3`, @@ -116,7 +119,7 @@ folders, should you wish so. Simply specify the folders you want to override in the `--folders` parameter. The ones you don't mention, will remain configured as default. -An example, if you want to use a folder `tables` to keeep you `up` scripts in, use the following argument to grate: +An example, if you want to use a folder `tables` to keep you `up` scripts in, use the following argument to grate: ```bash $ grate --folders up=tables @@ -128,6 +131,8 @@ grate processes the files in a standard set of directories in a fixed order for | Folder | Script type | Explanation | | ------ | ------- |------- | +| (-1. dropDatabase) | Anytime scripts | If you have the need for a custom `DROP DATABASE` script (used with the `--drop` command-line flag) | +| (0. createDatabase) | Anytime scripts | If you have the need for a custom `CREATE DATABASE` script, put it here, and it will be used instead of the default. | | 1. beforeMigration | Everytime scripts | If you have particular tasks you want to perform prior to any database migrations (custom logging? database backups? disable replication?) you can do it here. | | 2. alterDatabase | Anytime scripts | If you have scripts that need to alter the database config itself (rather than the _contents_ of the database) thjis is the place to do it. For example setting recovery modes, enabling query stores, etc etc | | 3. runAfterCreateDatabase | Anytime scripts | This directory is only processed if the database was created from scratch by grate. Maybe you need to add user accounts or similar? diff --git a/docs/ConfigurationOptions/index.md b/docs/ConfigurationOptions/index.md index 6268c247..ae46e7d0 100644 --- a/docs/ConfigurationOptions/index.md +++ b/docs/ConfigurationOptions/index.md @@ -21,7 +21,7 @@ grate --connectionstring="Server=(localdb)\MSSQLLocalDB;Integrated Security=true | Option | Default | Purpose | | ------ | ------- | ------- | | -c
-cs
--connectionstring
--connstring <connectionstring> | - | **REQUIRED** You now provide an entire connection string. ServerName and Database are obsolete. | -| -a
-acs
-csa
--adminconnectionstring
--adminconnstring <adminconnectionstring> | Same as --connectionstring | The connection string for connecting to master, if you want to create the database. | +| -a
-acs
-csa
--adminconnectionstring
--adminconnstring <adminconnectionstring> | The value provided via --connectionstring, with the target database replaced with a database that can be assumed to be present. For example, "master" for SQL Server. | Used when creating a new database, rather than migrating an existing one. | | -f
--files
--sqlfilesdirectory <sqlfilesdirectory> | . (current directory) | The directory where your SQL scripts are located | | --folders | Default folders as described in [Getting started](../GettingStarted.md) | Folder configuration, see [Folder configuration](FolderConfiguration.md) for details. | | -o
--output
--outputPath <outputPath> | %LOCALAPPDATA%/grate | This is where everything related to the migration is stored. This includes any backups, all items that ran, permission dumps, logs, etc. | diff --git a/docs/GettingGrate.md b/docs/GettingGrate.md index 54e01229..31fbc3ca 100644 --- a/docs/GettingGrate.md +++ b/docs/GettingGrate.md @@ -41,6 +41,10 @@ please install [dotnet 6](https://dotnet.microsoft.com/download/dotnet/6.0) grate is available on [winget](https://docs.microsoft.com/en-us/windows/package-manager/winget/). Simply `winget install erikbra.grate` for awesome! +## Homebrew + +grate is available as a Homebrew cask. Simply `brew install --cask erikbra/cask/grate` for awesomeness! + ## Notes Plans are afoot for more OS specific package managers, watch this space. diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index d6ed376b..0776927f 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -28,7 +28,7 @@ But don't be fooled, there's power in this simplicity due to a couple of key fac ## Examples -There are samples included in source control in the [`/examples/`](https://github.com/erikbra/grate/examples) directory, have a look and a play in there for some more info. +There are samples included in source control in the [`/examples/`](https://github.com/erikbra/grate/tree/main/examples) directory, have a look and a play in there for some more info. ## Script Types diff --git a/grate.unittests/Basic/CommandLineParsing/Basic_CommandLineParsing.cs b/grate.unittests/Basic/CommandLineParsing/Basic_CommandLineParsing.cs index f3a252cd..07773957 100644 --- a/grate.unittests/Basic/CommandLineParsing/Basic_CommandLineParsing.cs +++ b/grate.unittests/Basic/CommandLineParsing/Basic_CommandLineParsing.cs @@ -8,6 +8,7 @@ using grate.Commands; using grate.Configuration; using grate.Infrastructure; +using grate.unittests.TestInfrastructure; using NUnit.Framework; namespace grate.unittests.Basic.CommandLineParsing; @@ -65,6 +66,21 @@ public async Task AdminConnectionString(string argName) cfg?.AdminConnectionString.Should().Be(database); } + [TestCase(DatabaseType.mariadb)] + [TestCase(DatabaseType.oracle)] + [TestCase(DatabaseType.postgresql)] + [TestCase(DatabaseType.sqlite)] + [TestCase(DatabaseType.sqlserver)] + public async Task DefaultAdminConnectionString(DatabaseType databaseType) + { + var commandline = $"--connectionstring=;Database=jalla --databasetype={databaseType}"; + var cfg = await ParseGrateConfiguration(commandline); + + var masterDbName = TestConfig.GetTestContext(databaseType).MasterDatabase; + + cfg?.AdminConnectionString.Should().Be($";Database="+masterDbName); + } + [TestCase("-f ")] [TestCase("--files=")] [TestCase("--sqlfilesdirectory=")] @@ -284,6 +300,15 @@ public async Task TestDatabaseType(string args, DatabaseType expected) cfg?.DatabaseType.Should().Be(expected); } + [TestCase("", false)] + [TestCase("--ignoredirectorynames", true)] + [TestCase("--searchallinsteadoftraverse", true)] + [TestCase("--searchallsubdirectoriesinsteadoftraverse", true)] + public async Task IgnoreDirectoryNames(string args, bool expected) + { + var cfg = await ParseGrateConfiguration(args); + cfg?.IgnoreDirectoryNames.Should().Be(expected); + } private static async Task ParseGrateConfiguration(string commandline) { diff --git a/grate.unittests/Basic/Infrastructure/FileSystem_.cs b/grate.unittests/Basic/Infrastructure/FileSystem_.cs index 7b0dfd7f..fc49706f 100644 --- a/grate.unittests/Basic/Infrastructure/FileSystem_.cs +++ b/grate.unittests/Basic/Infrastructure/FileSystem_.cs @@ -31,6 +31,26 @@ public void Sorts_enumerated_files_on_filename_when_no_subfolders() files.First().FullName.Should().Be(Path.Combine(path.ToString(), filename1)); files.Last().FullName.Should().Be(Path.Combine(path.ToString(), filename2)); } + + [Test] + public void Sorts_enumerated_files_on_filename_without_extension_when_no_subfolders() + { + var parent = TestConfig.CreateRandomTempDirectory(); + var knownFolders = FoldersConfiguration.Default(null); + + var path = Wrap(parent, knownFolders[KnownFolderKeys.Up]!.Path); + + string filename1 = "01_any_filename_and_a_bit_longer.sql"; + string filename2 = "01_any_filename.sql"; + + TestConfig.WriteContent(path, filename1, "Whatever"); + TestConfig.WriteContent(path, filename2, "Whatever"); + + var files = FileSystem.GetFiles(path, "*.sql").ToList(); + + files.First().FullName.Should().Be(Path.Combine(path.ToString(), filename2)); + files.Last().FullName.Should().Be(Path.Combine(path.ToString(), filename1)); + } [Test] public void Sorts_enumerated_files_on_sub_path_when_subfolders_are_used() @@ -54,7 +74,30 @@ public void Sorts_enumerated_files_on_sub_path_when_subfolders_are_used() files.First().FullName.Should().Be(Path.Combine(folder1.ToString(), filename2)); files.Last().FullName.Should().Be(Path.Combine(folder2.ToString(), filename1)); } - + + [Test] + public void Sorts_enumerated_files_on_filename_when_directory_names_are_ignored() + { + var parent = TestConfig.CreateRandomTempDirectory(); + var knownFolders = FoldersConfiguration.Default(null); + + var path = Wrap(parent, knownFolders[KnownFolderKeys.Up]!.Path); + + var folder1 = new DirectoryInfo(Path.Combine(path.ToString(), "Init")); + var folder2 = new DirectoryInfo(Path.Combine(path.ToString(), "1.0")); + + string filename1 = "01_Schema.sql"; + string filename2 = "02_SomeChanges.sql"; + + TestConfig.WriteContent(folder1, filename1, "Whatever"); + TestConfig.WriteContent(folder2, filename2, "Whatever"); + + var files = FileSystem.GetFiles(path, "*.sql", true).ToList(); + + files.First().FullName.Should().Be(Path.Combine(folder1.ToString(), filename1)); + files.Last().FullName.Should().Be(Path.Combine(folder2.ToString(), filename2)); + } + protected static DirectoryInfo Wrap(DirectoryInfo root, string? subFolder) => new DirectoryInfo(Path.Combine(root.ToString(), subFolder ?? "")); diff --git a/grate.unittests/Basic/Infrastructure/FolderConfiguration/KnownFolders_CustomNames.cs b/grate.unittests/Basic/Infrastructure/FolderConfiguration/KnownFolders_CustomNames.cs index d46f813d..8292a7dd 100644 --- a/grate.unittests/Basic/Infrastructure/FolderConfiguration/KnownFolders_CustomNames.cs +++ b/grate.unittests/Basic/Infrastructure/FolderConfiguration/KnownFolders_CustomNames.cs @@ -69,6 +69,7 @@ TransactionHandling transactionHandling private static readonly IKnownFolderNames OverriddenFolderNames = new KnownFolderNames() { BeforeMigration = "beforeMigration" + Random.GetString(8), + CreateDatabase = "createDatabase" + Random.GetString(8), AlterDatabase = "alterDatabase" + Random.GetString(8), RunAfterCreateDatabase = "runAfterCreateDatabase" + Random.GetString(8), RunBeforeUp = "runBeforeUp" + Random.GetString(8), diff --git a/grate.unittests/Basic/Infrastructure/FolderConfiguration/KnownFolders_Default.cs b/grate.unittests/Basic/Infrastructure/FolderConfiguration/KnownFolders_Default.cs index b1399d31..1140e150 100644 --- a/grate.unittests/Basic/Infrastructure/FolderConfiguration/KnownFolders_Default.cs +++ b/grate.unittests/Basic/Infrastructure/FolderConfiguration/KnownFolders_Default.cs @@ -45,12 +45,12 @@ public void Returns_folders_in_current_order() [Test] [TestCaseSource(nameof(ExpectedKnownFolderNames))] public void Has_expected_folder_configuration( - MigrationsFolder folder, - string expectedName, - MigrationType expectedType, - ConnectionType expectedConnectionType, - TransactionHandling transactionHandling - ) + MigrationsFolder folder, + string expectedName, + MigrationType expectedType, + ConnectionType expectedConnectionType, + TransactionHandling transactionHandling + ) { var root = Root.ToString(); @@ -94,11 +94,11 @@ private static TestCaseData GetTestCase( ) => new TestCaseData(folder, expectedName, expectedType, expectedConnectionType, transactionHandling) .SetArgDisplayNames( - migrationsFolderDefinitionName, - expectedName, - expectedType.ToString(), - "conn: " + expectedConnectionType, - "tran: " + transactionHandling - ); + migrationsFolderDefinitionName, + expectedName, + expectedType.ToString(), + "conn: " + expectedConnectionType, + "tran: " + transactionHandling + ); } diff --git a/grate.unittests/Basic/Infrastructure/Oracle/Statement_Splitting/BatchSplitterReplacer_.cs b/grate.unittests/Basic/Infrastructure/Oracle/Statement_Splitting/BatchSplitterReplacer_.cs new file mode 100644 index 00000000..0909624b --- /dev/null +++ b/grate.unittests/Basic/Infrastructure/Oracle/Statement_Splitting/BatchSplitterReplacer_.cs @@ -0,0 +1,563 @@ +using grate.Infrastructure; +using grate.Migration; +using grate.unittests.TestInfrastructure; +using Microsoft.Extensions.Logging.Abstractions; +using NUnit.Framework; + +// ReSharper disable InconsistentNaming + +namespace grate.unittests.Basic.Infrastructure.Oracle.Statement_Splitting; + +[TestFixture] +[Category("Basic")] +public class BatchSplitterReplacer_ +{ + private const string Batch_terminator_replacement_string = StatementSplitter.BatchTerminatorReplacementString; + + private const string Symbols_to_check = "`~!@#$%^&*()-_+=,.;:'\"[]\\/?<>"; + private const string Words_to_check = "abcdefghijklmnopqrstuvwzyz0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + private static readonly IDatabase Database = new OracleDatabase(NullLogger.Instance); + private static BatchSplitterReplacer Replacer => new(Database.StatementSeparatorRegex, StatementSplitter.BatchTerminatorReplacementString); + + public class should_replace_on + { + [Test] + public void full_statement_without_issue() + { + string sql_to_match = OracleSplitterContext.FullSplitter.PLSqlStatement; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(OracleSplitterContext.FullSplitter.PLSqlStatementScrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_space() + { + const string sql_to_match = @" / "; + string expected_scrubbed = @" " + Batch_terminator_replacement_string + @" "; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_tab() + { + string sql_to_match = @" /" + "\t"; + string expected_scrubbed = @" " + Batch_terminator_replacement_string + "\t"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_by_itself() + { + const string sql_to_match = @"/"; + string expected_scrubbed = Batch_terminator_replacement_string; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_starting_file() + { + const string sql_to_match = @"/ +whatever"; + string expected_scrubbed = Batch_terminator_replacement_string + @" +whatever"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_new_line() + { + const string sql_to_match = @" / +"; + string expected_scrubbed = @" " + Batch_terminator_replacement_string + @" +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_one_new_line_after_double_dash_comments() + { + const string sql_to_match = + @"-- +/ +"; + string expected_scrubbed = + @"-- +" + Batch_terminator_replacement_string + @" +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_one_new_line_after_double_dash_comments_and_words() + { + string sql_to_match = @"-- " + Words_to_check + @" +/ +"; + string expected_scrubbed = @"-- " + Words_to_check + @" +" + Batch_terminator_replacement_string + @" +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_new_line_after_double_dash_comments_and_symbols() + { + string sql_to_match = @"-- " + Symbols_to_check + @" +/ +"; + string expected_scrubbed = @"-- " + Symbols_to_check + @" +" + Batch_terminator_replacement_string + @" +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_on_its_own_line() + { + const string sql_to_match = @" +/ +"; + string expected_scrubbed = @" +" + Batch_terminator_replacement_string + @" +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_no_line_terminator() + { + const string sql_to_match = @" / "; + string expected_scrubbed = @" " + Batch_terminator_replacement_string + @" "; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_words_before() + { + string sql_to_match = Words_to_check + @" / +"; + string expected_scrubbed = Words_to_check + @" " + Batch_terminator_replacement_string + @" +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_symbols_and_words_before() + { + string sql_to_match = Symbols_to_check + Words_to_check + @" / +"; + string expected_scrubbed = Symbols_to_check + Words_to_check + @" " + + Batch_terminator_replacement_string + @" +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_words_and_symbols_before() + { + string sql_to_match = Words_to_check + Symbols_to_check + @" / +"; + string expected_scrubbed = Words_to_check + Symbols_to_check + @" " + + Batch_terminator_replacement_string + @" +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_words_after_on_the_same_line() + { + string sql_to_match = @" / " + Words_to_check; + string expected_scrubbed = @" " + Batch_terminator_replacement_string + @" " + Words_to_check; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_words_after_on_the_same_line_including_symbols() + { + string sql_to_match = @" / " + Words_to_check + Symbols_to_check; + string expected_scrubbed = @" " + Batch_terminator_replacement_string + @" " + Words_to_check + + Symbols_to_check; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_words_before_and_after_on_the_same_line() + { + string sql_to_match = Words_to_check + @" / " + Words_to_check; + string expected_scrubbed = Words_to_check + @" " + Batch_terminator_replacement_string + @" " + + Words_to_check; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_words_before_and_after_on_the_same_line_including_symbols() + { + string sql_to_match = Words_to_check + Symbols_to_check.Replace("'", "").Replace("\"", "") + + " / BOB" + Symbols_to_check; + string expected_scrubbed = Words_to_check + Symbols_to_check.Replace("'", "").Replace("\"", "") + + " " + Batch_terminator_replacement_string + " BOB" + Symbols_to_check; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_after_double_dash_comment_with_single_quote_and_single_quote_after_slash() + { + string sql_to_match = Words_to_check + @" -- ' +/ +select '' +/"; + string expected_scrubbed = Words_to_check + @" -- ' +" + Batch_terminator_replacement_string + @" +select '' +" + Batch_terminator_replacement_string; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_comment_after() + { + string sql_to_match = " / -- comment"; + string expected_scrubbed = " " + Batch_terminator_replacement_string + " -- comment"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_semicolon_directly_after() + { + string sql_to_match = "jalla /;"; + string expected_scrubbed = "jalla " + Batch_terminator_replacement_string + ";"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + } + + public class should_not_replace_on + { + + [Test] + public void slash_when_slash_is_the_last_part_of_the_last_word_on_a_line() + { + string sql_to_match = Words_to_check + @"/ +"; + string expected_scrubbed = Words_to_check + @"/ +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_double_dash_comment_starting_line() + { + string sql_to_match = @"--/ +"; + string expected_scrubbed = @"--/ +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_double_dash_comment_and_space_starting_line() + { + string sql_to_match = @"-- / +"; + string expected_scrubbed = @"-- / +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_double_dash_comment_and_space_starting_line_and_words_after_slash() + { + string sql_to_match = @"-- / " + Words_to_check + @" +"; + string expected_scrubbed = @"-- / " + Words_to_check + @" +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_double_dash_comment_and_space_starting_line_and_symbols_after_slash() + { + string sql_to_match = @"-- / " + Symbols_to_check + @" +"; + string expected_scrubbed = @"-- / " + Symbols_to_check + @" +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_double_dash_comment_and_tab_starting_line() + { + string sql_to_match = "--" + "\t" + @"/ +"; + string expected_scrubbed = @"--" + "\t" + @"/ +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_double_dash_comment_and_tab_starting_line_and_words_after_slash() + { + string sql_to_match = @"--" + "\t" + @"/ " + Words_to_check + @" +"; + string expected_scrubbed = @"--" + "\t" + @"/ " + Words_to_check + @" +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_double_dash_comment_and_tab_starting_line_and_symbols_after_slash() + { + string sql_to_match = @"--" + "\t" + @"/ " + Symbols_to_check + @" +"; + string expected_scrubbed = @"--" + "\t" + @"/ " + Symbols_to_check + @" +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_double_dash_comment_starting_line_with_words_before_slash() + { + string sql_to_match = @"-- " + Words_to_check + @" / +"; + string expected_scrubbed = @"-- " + Words_to_check + @" / +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_when_between_tick_marks() + { + const string sql_to_match = @"' / + '"; + const string expected_scrubbed = @"' / + '"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void + slash_when_between_tick_marks_with_symbols_and_words_before_ending_on_same_line() + { + string sql_to_match = @"' " + Symbols_to_check.Replace("'", string.Empty) + Words_to_check + @" /'"; + string expected_scrubbed = + @"' " + Symbols_to_check.Replace("'", string.Empty) + Words_to_check + @" /'"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_when_between_tick_marks_with_symbols_and_words_before() + { + string sql_to_match = @"' " + Symbols_to_check.Replace("'", string.Empty) + Words_to_check + @" / + '"; + string expected_scrubbed = @"' " + Symbols_to_check.Replace("'", string.Empty) + Words_to_check + @" / + '"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_when_between_tick_marks_with_symbols_and_words_after() + { + string sql_to_match = @"' / + " + Symbols_to_check.Replace("'", string.Empty) + Words_to_check + @"'"; + string expected_scrubbed = @"' / + " + Symbols_to_check.Replace("'", string.Empty) + Words_to_check + @"'"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_with_double_dash_comment_starting_line_with_symbols_before_slash() + { + string sql_to_match = @"--" + Symbols_to_check + @" / +"; + string expected_scrubbed = @"--" + Symbols_to_check + @" / +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void + slash_with_double_dash_comment_starting_line_with_words_and_symbols_before_slash() + { + string sql_to_match = @"--" + Symbols_to_check + Words_to_check + @" / +"; + string expected_scrubbed = @"--" + Symbols_to_check + Words_to_check + @" / +"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_inside_of_comments() + { + string sql_to_match = @"/* / */"; + string expected_scrubbed = @"/* / */"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_inside_of_comments_with_a_line_break() + { + string sql_to_match = @"/* / +*/"; + string expected_scrubbed = @"/* / +*/"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_inside_of_comments_with_words_before() + { + string sql_to_match = + @"/* +" + Words_to_check + @" / + +*/"; + string expected_scrubbed = + @"/* +" + Words_to_check + @" / + +*/"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_inside_of_comments_with_words_before_on_a_different_line() + { + string sql_to_match = + @"/* +" + Words_to_check + @" +/ + +*/"; + string expected_scrubbed = + @"/* +" + Words_to_check + @" +/ + +*/"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_inside_of_comments_with_words_before_and_after_on_different_lines() + { + string sql_to_match = + @"/* +" + Words_to_check + @" +/ + +" + Words_to_check + @" +*/"; + string expected_scrubbed = + @"/* +" + Words_to_check + @" +/ + +" + Words_to_check + @" +*/"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + + [Test] + public void slash_inside_of_comments_with_symbols_after_on_different_lines() + { + string sql_to_match = + @"/* +/ + +" + Symbols_to_check + @" +*/"; + string expected_scrubbed = + @"/* +/ + +" + Symbols_to_check + @" +*/"; + TestContext.WriteLine(sql_to_match); + string sql_statement_scrubbed = Replacer.Replace(sql_to_match); + Assert.AreEqual(expected_scrubbed, sql_statement_scrubbed); + } + } + +} diff --git a/grate.unittests/Basic/Infrastructure/Oracle/Statement_Splitting/StatementSplitter_.cs b/grate.unittests/Basic/Infrastructure/Oracle/Statement_Splitting/StatementSplitter_.cs new file mode 100644 index 00000000..d5bf5a6c --- /dev/null +++ b/grate.unittests/Basic/Infrastructure/Oracle/Statement_Splitting/StatementSplitter_.cs @@ -0,0 +1,32 @@ +using FluentAssertions; +using grate.Infrastructure; +using grate.Migration; +using Microsoft.Extensions.Logging.Abstractions; +using NUnit.Framework; + +namespace grate.unittests.Basic.Infrastructure.Oracle.Statement_Splitting; + +[TestFixture] +[Category("Basic")] +// ReSharper disable once InconsistentNaming +public class StatementSplitter_ +{ + private static readonly IDatabase Database = new OracleDatabase(NullLogger.Instance); + private static readonly StatementSplitter Splitter = new(Database.StatementSeparatorRegex); + + [Test] + public void Splits_and_removes_GO_statements() + { + var original = @" +SELECT * FROM v$version WHERE banner LIKE 'Oracle%'; + + +/ +SELECT 1 +"; + var batches = Splitter.Split(original); + + batches.Should().HaveCount(2); + } + +} diff --git a/grate.unittests/Basic/Infrastructure/SqlServer/SqlServerDatabase_.cs b/grate.unittests/Basic/Infrastructure/SqlServer/SqlServerDatabase_.cs new file mode 100644 index 00000000..52df01df --- /dev/null +++ b/grate.unittests/Basic/Infrastructure/SqlServer/SqlServerDatabase_.cs @@ -0,0 +1,50 @@ +using System.Data.Common; +using System.Threading.Tasks; +using FluentAssertions; +using grate.Configuration; +using grate.Migration; +using grate.unittests.TestInfrastructure; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using NUnit.Framework; + +namespace grate.unittests.Basic.Infrastructure.SqlServer; + +// ReSharper disable once InconsistentNaming +public class SqlServerDatabase_ +{ + [Test] + public async Task Disables_pooling_if_not_explicitly_set_in_connection_string() + { + var connStr = "Server=dummy"; + var cfg = new GrateConfiguration() { ConnectionString = connStr }; + var sqlServerDatabase = new InspectableSqlServerDatabase(); + await sqlServerDatabase.InitializeConnections(cfg); + + var conn = sqlServerDatabase.GetConnection(); + var builder = new SqlConnectionStringBuilder(conn.ConnectionString); + builder.Pooling.Should().BeFalse(); + } + + [Test] + public async Task Leaves_pooling_as_configured_if_set_explicitly_in_connection_string() + { + var connStr = "Server=dummy;Pooling=true"; + var cfg = new GrateConfiguration() { ConnectionString = connStr }; + var sqlServerDatabase = new InspectableSqlServerDatabase(); + await sqlServerDatabase.InitializeConnections(cfg); + + var conn = sqlServerDatabase.GetConnection(); + var builder = new SqlConnectionStringBuilder(conn.ConnectionString); + builder.Pooling.Should().BeTrue(); + } + + private class InspectableSqlServerDatabase : SqlServerDatabase + { + public InspectableSqlServerDatabase() : base(TestConfig.LogFactory.CreateLogger()) + { + } + + public DbConnection GetConnection() => base.Connection; + } +} diff --git a/grate.unittests/Basic/Infrastructure/SqlServer/Statement_Splitting/BatchSplitterReplacer_.cs b/grate.unittests/Basic/Infrastructure/SqlServer/Statement_Splitting/BatchSplitterReplacer_.cs index 4cc55838..52510e3d 100644 --- a/grate.unittests/Basic/Infrastructure/SqlServer/Statement_Splitting/BatchSplitterReplacer_.cs +++ b/grate.unittests/Basic/Infrastructure/SqlServer/Statement_Splitting/BatchSplitterReplacer_.cs @@ -25,10 +25,10 @@ public class should_replace_on [Test] public void full_statement_without_issue() { - string sql_to_match = SplitterContext.FullSplitter.tsql_statement; + string sql_to_match = SqlServerSplitterContext.FullSplitter.tsql_statement; TestContext.WriteLine(sql_to_match); string sql_statement_scrubbed = Replacer.Replace(sql_to_match); - Assert.AreEqual(SplitterContext.FullSplitter.tsql_statement_scrubbed, sql_statement_scrubbed); + Assert.AreEqual(SqlServerSplitterContext.FullSplitter.tsql_statement_scrubbed, sql_statement_scrubbed); } [Test] diff --git a/grate.unittests/Basic/Migration.cs b/grate.unittests/Basic/Migration.cs new file mode 100644 index 00000000..131de20f --- /dev/null +++ b/grate.unittests/Basic/Migration.cs @@ -0,0 +1,75 @@ +using System.IO; +using System.Threading.Tasks; +using grate.Configuration; +using grate.Infrastructure; +using grate.Migration; +using grate.unittests.TestInfrastructure; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NUnit.Framework; + +namespace grate.unittests.Basic; + +public class Migration +{ + private readonly ILogger _logger; + + public Migration() + { + _logger = Substitute.For>(); + } + + [Test] + public async Task Does_not_output_no_sql_run_in_dryrun_mode() + { + var dbMigrator = GetDbMigrator(true); + var migrator = new GrateMigrator(_logger, dbMigrator); + await migrator.Migrate(); + _logger.DidNotReceive().LogInformation(" No sql run, either an empty folder, or all files run against destination previously."); + } + + [Test] + public async Task Outputs_no_sql_run_in_live_mode() + { + var dbMigrator = GetDbMigrator(false); + var migrator = new GrateMigrator(_logger, dbMigrator); + await migrator.Migrate(); + _logger.Received().LogInformation(" No sql run, either an empty folder, or all files run against destination previously."); + } + + protected static DirectoryInfo Wrap(DirectoryInfo root, string? subFolder) => + new DirectoryInfo(Path.Combine(root.ToString(), subFolder ?? "")); + + private static IDbMigrator GetDbMigrator(bool dryRun) + { + var dbMigrator = Substitute.For(); + dbMigrator.RunSql( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ).ReturnsForAnyArgs(false); + var parent = TestConfig.CreateRandomTempDirectory(); + var knownFolders = FoldersConfiguration.Default(); + + var path = Wrap(parent, knownFolders[KnownFolderKeys.Up]!.Path); + + var folder1 = new DirectoryInfo(Path.Combine(path.ToString(), "01_sub", "folder", "long", "way")); + string filename1 = "01_any_filename.sql"; + TestConfig.WriteContent(folder1, filename1, "Whatever"); + + var configuration = new GrateConfiguration() + { + NonInteractive = true, + SqlFilesDirectory = parent, + DryRun = dryRun + }; + dbMigrator.Configuration.Returns(configuration); + + return dbMigrator; + } + +} diff --git a/grate.unittests/Generic/GenericDatabase.cs b/grate.unittests/Generic/GenericDatabase.cs index be6654e9..aa6586b9 100644 --- a/grate.unittests/Generic/GenericDatabase.cs +++ b/grate.unittests/Generic/GenericDatabase.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Data.Common; +using System.IO; using System.Linq; using System.Threading.Tasks; using System.Transactions; @@ -8,7 +9,9 @@ using grate.Configuration; using grate.Migration; using grate.unittests.TestInfrastructure; +using Microsoft.Data.SqlClient; using NUnit.Framework; +using static System.StringSplitOptions; namespace grate.unittests.Generic; @@ -28,6 +31,39 @@ public async Task Is_created_if_confed_and_it_does_not_exist() IEnumerable databases = await GetDatabases(); databases.Should().Contain(db); } + + [Test] + public virtual async Task Is_created_with_custom_script_if_custom_create_database_folder_exists() + { + var scriptedDatabase = "CUSTOMSCRIPTEDDATABASE"; + var confedDatabase = "DEFAULTDATABASE"; + + var config = GetConfiguration(confedDatabase, true); + var password = Context.AdminConnectionString + .Split(";", TrimEntries | RemoveEmptyEntries) + .SingleOrDefault(entry => entry.StartsWith("Password") || entry.StartsWith("Pwd"))? + .Split("=", TrimEntries | RemoveEmptyEntries) + .Last(); + + var customScript = Context.Syntax.CreateDatabase(scriptedDatabase, password); + TestConfig.WriteContent(Wrap(config.SqlFilesDirectory, config.Folders?.CreateDatabase?.Path), "createDatabase.sql", customScript); + try + { + await using var migrator = GetMigrator(config); + await migrator.Migrate(); + } + catch (DbException) + { + //Do nothing because database name is wrong due to custom script + } + + File.Delete(Path.Join(Wrap(config.SqlFilesDirectory, config.Folders?.CreateDatabase?.Path).ToString(), "createDatabase.sql")); + + // The database should have been created by the custom script + IEnumerable databases = (await GetDatabases()).ToList(); + databases.Should().Contain(scriptedDatabase); + databases.Should().NotContain(confedDatabase); + } [Test] public async Task Is_not_created_if_not_confed() @@ -91,23 +127,6 @@ public async Task Does_not_need_admin_connection_if_database_already_exists(stri Assert.DoesNotThrowAsync(() => migrator.Migrate()); } - [Test] - public async Task Does_not_needlessly_apply_case_sensitive_database_name_checks_Issue_167() - { - // There's a bug where if the database name specified by the user differs from the actual database only by case then - // Grate currently attempts to create the database again, only for it to fail on the DBMS (Sql Server bug only). - - var db = "CASEDATABASE"; - await CreateDatabase(db); - - // Check that the database has been created - IEnumerable databasesBeforeMigration = await GetDatabases(); - databasesBeforeMigration.Should().Contain(db); - - await using var migrator = GetMigrator(GetConfiguration(db.ToLower(), true)); // ToLower is important here, this reproduces the bug in #167 - // There should be no errors running the migration - Assert.DoesNotThrowAsync(() => migrator.Migrate()); - } protected Task CreateDatabase(string db) => CreateDatabaseFromConnectionString(db, Context.ConnectionString(db)); @@ -172,9 +191,9 @@ protected virtual async Task> GetDatabases() protected virtual bool ThrowOnMissingDatabase => true; - private GrateMigrator GetMigrator(GrateConfiguration config) => Context.GetMigrator(config); + protected GrateMigrator GetMigrator(GrateConfiguration config) => Context.GetMigrator(config); - private GrateConfiguration GetConfiguration(string databaseName, bool createDatabase) + protected GrateConfiguration GetConfiguration(string databaseName, bool createDatabase) => GetConfiguration(databaseName, createDatabase, Context.AdminConnectionString); @@ -197,4 +216,8 @@ private GrateConfiguration GetConfiguration(bool createDatabase, string? connect }; } + + protected static DirectoryInfo Wrap(DirectoryInfo root, string? subFolder) => + new DirectoryInfo(Path.Combine(root.ToString(), subFolder ?? "")); + } diff --git a/grate.unittests/Generic/GenericMigrationTables.cs b/grate.unittests/Generic/GenericMigrationTables.cs index 8cf0736e..b66cc018 100644 --- a/grate.unittests/Generic/GenericMigrationTables.cs +++ b/grate.unittests/Generic/GenericMigrationTables.cs @@ -98,7 +98,90 @@ public async Task Migration_does_not_fail_if_table_already_exists(string tableNa Assert.DoesNotThrowAsync(() => migrator.Migrate()); } } + + [TestCase("version")] + [TestCase("vErSiON")] + public async Task Does_not_create_Version_table_if_it_exists_with_another_casing(string existingTable) + { + await CheckTableCasing("Version", existingTable, (config, name) => config.VersionTableName = name); + } + + [TestCase("scriptsrun")] + [TestCase("SCRiptSrUN")] + public async Task Does_not_create_ScriptsRun_table_if_it_exists_with_another_casing(string existingTable) + { + await CheckTableCasing("ScriptsRun", existingTable, (config, name) => config.ScriptsRunTableName = name); + } + + [TestCase("scriptsrunerrors")] + [TestCase("ScripTSRunErrors")] + public async Task Does_not_create_ScriptsRunErrors_table_if_it_exists_with_another_casing(string existingTable) + { + await CheckTableCasing("ScriptsRunErrors", existingTable, (config, name) => config.ScriptsRunErrorsTableName = name); + } + + protected virtual async Task CheckTableCasing(string tableName, string funnyCasing, Action setTableName) + { + var db = TestConfig.RandomDatabase(); + + var parent = TestConfig.CreateRandomTempDirectory(); + var knownFolders = FoldersConfiguration.Default(); + + // Set the version table name to be lower-case first, and run one migration. + var config = Context.GetConfiguration(db, parent, knownFolders); + + setTableName(config, funnyCasing); + + await using (var migrator = Context.GetMigrator(config)) + { + await migrator.Migrate(); + } + + // Check that the table is indeed created with lower-case + var errorCaseCountAfterFirstMigration = await TableCountIn(db, funnyCasing); + var normalCountAfterFirstMigration = await TableCountIn(db, tableName); + Assert.Multiple(() => + { + errorCaseCountAfterFirstMigration.Should().Be(1); + normalCountAfterFirstMigration.Should().Be(0); + }); + + // Run migration again - make sure it does not create the table with different casing too + setTableName(config, tableName); + await using (var migrator = Context.GetMigrator(config)) + { + await migrator.Migrate(); + } + + var errorCaseCountAfterSecondMigration = await TableCountIn(db, funnyCasing); + var normalCountAfterSecondMigration = await TableCountIn(db, tableName); + Assert.Multiple(() => + { + errorCaseCountAfterSecondMigration.Should().Be(1); + normalCountAfterSecondMigration.Should().Be(0); + }); + + } + + private async Task TableCountIn(string db, string tableName) + { + var schemaName = Context.DefaultConfiguration.SchemaName; + var supportsSchemas = Context.DatabaseMigrator.SupportsSchemas; + var fullTableName = supportsSchemas ? tableName : Context.Syntax.TableWithSchema(schemaName, tableName); + var tableSchema = supportsSchemas ? schemaName : db; + + int count; + string countSql = CountTableSql(tableSchema, fullTableName); + + await using (var conn = Context.GetDbConnection(Context.ConnectionString(db))) + { + count = await conn.ExecuteScalarAsync(countSql); + } + + return count; + } + [Test()] public async Task Inserts_version_in_version_table() { @@ -156,4 +239,14 @@ protected static DirectoryInfo MakeSurePathExists(DirectoryInfo? path) private static DirectoryInfo Wrap(DirectoryInfo root, string? relativePath) => new(Path.Combine(root.ToString(), relativePath ?? "")); + protected virtual string CountTableSql(string schemaName, string tableName) + { + return $@" +SELECT count(table_name) FROM INFORMATION_SCHEMA.TABLES +WHERE +table_schema = '{schemaName}' AND +table_name = '{tableName}' +"; + } + } diff --git a/grate.unittests/Generic/Running_MigrationScripts/Anytime_scripts.cs b/grate.unittests/Generic/Running_MigrationScripts/Anytime_scripts.cs index fb3b1240..bf11d7a7 100644 --- a/grate.unittests/Generic/Running_MigrationScripts/Anytime_scripts.cs +++ b/grate.unittests/Generic/Running_MigrationScripts/Anytime_scripts.cs @@ -70,7 +70,7 @@ public async Task Are_run_again_if_changed_between_runs() } string[] scripts; - string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; + string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")} ORDER BY id"; await using (var conn = Context.CreateDbConnection(db)) { diff --git a/grate.unittests/Generic/Running_MigrationScripts/Failing_Scripts.cs b/grate.unittests/Generic/Running_MigrationScripts/Failing_Scripts.cs index e977b25e..ba8cafb5 100644 --- a/grate.unittests/Generic/Running_MigrationScripts/Failing_Scripts.cs +++ b/grate.unittests/Generic/Running_MigrationScripts/Failing_Scripts.cs @@ -1,4 +1,5 @@ -using System.Data.Common; +using System; +using System.Data.Common; using System.IO; using System.Linq; using System.Runtime.CompilerServices; @@ -73,6 +74,42 @@ public async Task Inserts_Failed_Scripts_Into_ScriptRunErrors_Table() scripts.Should().HaveCount(1); } + [Test()] + public async Task Inserts_Large_Failed_Scripts_Into_ScriptRunErrors_Table() + { + var db = TestConfig.RandomDatabase(); + + var parent = TestConfig.CreateRandomTempDirectory(); + var knownFolders = FoldersConfiguration.Default(null); + GrateMigrator? migrator; + + CreateLongInvalidSql(parent, knownFolders[Up]); + + await using (migrator = Context.GetMigrator(db, parent, knownFolders)) + { + try + { + await migrator.Migrate(); + } + catch (MigrationFailed) + { + } + } + + string fileContent = await File.ReadAllTextAsync(Path.Combine(parent.ToString(), knownFolders[Up]!.Path, "2_failing.sql")); + + string[] scripts; + string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRunErrors")}"; + + using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled)) + { + await using var conn = Context.CreateDbConnection(db); + scripts = (await conn.QueryAsync(sql)).ToArray(); + } + + scripts.First().Should().Be(fileContent); + } + [Test] public void Ensure_Command_Timeout_Fires() { @@ -239,6 +276,13 @@ protected static void CreateInvalidSql(DirectoryInfo root, MigrationsFolder? fol WriteSql(path, "2_failing.sql", dummySql); } + protected void CreateLongInvalidSql(DirectoryInfo root, MigrationsFolder? folder) + { + var dummySql = CreateLongComment(8192) + Environment.NewLine + "SELECT TOP"; + var path = MakeSurePathExists(root, folder); + WriteSql(path, "2_failing.sql", dummySql); + } + private static readonly DirectoryInfo Root = TestConfig.CreateRandomTempDirectory(); private static readonly IFoldersConfiguration Folders = FoldersConfiguration.Default(null); diff --git a/grate.unittests/Generic/Running_MigrationScripts/MigrationsScriptsBase.cs b/grate.unittests/Generic/Running_MigrationScripts/MigrationsScriptsBase.cs index b6595b2a..996b2b2e 100644 --- a/grate.unittests/Generic/Running_MigrationScripts/MigrationsScriptsBase.cs +++ b/grate.unittests/Generic/Running_MigrationScripts/MigrationsScriptsBase.cs @@ -1,4 +1,6 @@ -using System.IO; +using System; +using System.IO; +using System.Text; using grate.Configuration; using grate.unittests.TestInfrastructure; @@ -19,6 +21,40 @@ protected void CreateDummySql(DirectoryInfo? path, string filename = "1_jalla.sq var dummySql = Context.Sql.SelectVersion; WriteSql(path, filename, dummySql); } + + protected void CreateLargeDummySql(DirectoryInfo? path, int size = 8192, string filename = "1_very_large_file.sql") + { + var longComment = CreateLongComment(size); + + var dummySql = longComment + Environment.NewLine + Context.Sql.SelectVersion; + WriteSql(path, filename, dummySql); + } + + protected string CreateLongComment(int size) + { + // Line comment plus blank, plus new line, plus text should be 80 together. + int lineLen = 80 - Context.Sql.LineComment.Length - 1 - Environment.NewLine.Length; + var numLines = size / lineLen; + var rest = size - (lineLen * numLines); + + var filler = new string('Æ', Math.Min(lineLen, size)); + + var builder = new StringBuilder(lineLen * numLines + rest); + for (var i = 0; i < numLines; i++) + { + builder.Append(Context.Sql.LineComment); + builder.Append(' '); + builder.AppendLine(filler); + } + if (rest > 0) + { + builder.Append(Context.Sql.LineComment); + builder.Append(' '); + builder.AppendLine(new string('Ø', rest)); + } + + return builder.ToString(); + } protected void WriteSomeOtherSql(DirectoryInfo? path, string filename = "1_jalla.sql") { diff --git a/grate.unittests/Generic/Running_MigrationScripts/Order_Of_Scripts.cs b/grate.unittests/Generic/Running_MigrationScripts/Order_Of_Scripts.cs index 604399af..24b4c3c5 100644 --- a/grate.unittests/Generic/Running_MigrationScripts/Order_Of_Scripts.cs +++ b/grate.unittests/Generic/Running_MigrationScripts/Order_Of_Scripts.cs @@ -27,7 +27,7 @@ public async Task Is_as_expected() } string[] scripts; - string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; + string sql = $"SELECT script_name FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")} ORDER BY id"; await using (var conn = Context.CreateDbConnection(db)) { diff --git a/grate.unittests/Generic/Running_MigrationScripts/ScriptsRun_Table.cs b/grate.unittests/Generic/Running_MigrationScripts/ScriptsRun_Table.cs index 407931f4..5022823b 100644 --- a/grate.unittests/Generic/Running_MigrationScripts/ScriptsRun_Table.cs +++ b/grate.unittests/Generic/Running_MigrationScripts/ScriptsRun_Table.cs @@ -122,10 +122,38 @@ public async Task Does_not_overwrite_scripts_from_different_folders_with_last_co second.script_name.Should().Be($"sub/dolder/gong/way/{filename}"); second.text_of_script.Should().Be(Context.Syntax.CurrentDatabase); }); - - - } + [Test()] + public async Task Can_handle_large_scripts() + { + var db = TestConfig.RandomDatabase(); + + var parent = TestConfig.CreateRandomTempDirectory(); + var knownFolders = FoldersConfiguration.Default(null); + GrateMigrator? migrator; + + var folder = new DirectoryInfo(Path.Combine(parent.ToString(), knownFolders[Up]!.Path)); + + const string filename = "large_file.sql"; + + CreateLargeDummySql(folder, filename: filename); + await using (migrator = Context.GetMigrator(db, parent, knownFolders)) + { + await migrator.Migrate(); + } + + string fileContent = await File.ReadAllTextAsync(Path.Combine(folder.ToString(), filename)); + + string[] scripts; + string sql = $"SELECT text_of_script FROM {Context.Syntax.TableWithSchema("grate", "ScriptsRun")}"; + + await using (var conn = Context.CreateDbConnection(db)) + { + scripts = (await conn.QueryAsync(sql)).ToArray(); + } + + scripts.First().Should().Be(fileContent); + } } diff --git a/grate.unittests/Oracle/MigrationTables.cs b/grate.unittests/Oracle/MigrationTables.cs index 0e10bb88..5902728b 100644 --- a/grate.unittests/Oracle/MigrationTables.cs +++ b/grate.unittests/Oracle/MigrationTables.cs @@ -1,3 +1,6 @@ +using System; +using System.Threading.Tasks; +using grate.Configuration; using grate.unittests.TestInfrastructure; using NUnit.Framework; @@ -5,7 +8,21 @@ namespace grate.unittests.Oracle; [TestFixture] [Category("Oracle")] -public class MigrationTables: Generic.GenericMigrationTables +public class MigrationTables : Generic.GenericMigrationTables { protected override IGrateTestContext Context => GrateTestContext.Oracle; -} \ No newline at end of file + + protected override Task CheckTableCasing(string tableName, string funnyCasing, Action setTableName) + { + Assert.Ignore("Oracle has never been case-sensitive for grate. No need to introduce that now."); + return Task.CompletedTask; + } + + protected override string CountTableSql(string schemaName, string tableName) + { + return $@" +SELECT COUNT(table_name) FROM user_tables +WHERE +lower(table_name) = '{tableName.ToLowerInvariant()}'"; + } +} diff --git a/grate.unittests/SqLite/Database.cs b/grate.unittests/SqLite/Database.cs index 8c7272c1..4e4e70b6 100644 --- a/grate.unittests/SqLite/Database.cs +++ b/grate.unittests/SqLite/Database.cs @@ -42,5 +42,9 @@ protected override async Task> GetDatabases() return await ValueTask.FromResult(dbNames); } + [Ignore("SQLite does not support custom database creation script")] + public override Task Is_created_with_custom_script_if_custom_create_database_folder_exists() => + Task.CompletedTask; + protected override bool ThrowOnMissingDatabase => false; } diff --git a/grate.unittests/SqLite/MigrationTables.cs b/grate.unittests/SqLite/MigrationTables.cs index e9a9495b..5cb536d2 100644 --- a/grate.unittests/SqLite/MigrationTables.cs +++ b/grate.unittests/SqLite/MigrationTables.cs @@ -8,4 +8,13 @@ namespace grate.unittests.Sqlite; public class MigrationTables: Generic.GenericMigrationTables { protected override IGrateTestContext Context => GrateTestContext.Sqlite; -} \ No newline at end of file + + protected override string CountTableSql(string schemaName, string tableName) + { + return $@" +SELECT COUNT(name) FROM sqlite_master +WHERE type ='table' AND +name = '{tableName}'; +"; + } +} diff --git a/grate.unittests/SqlServer/Database.cs b/grate.unittests/SqlServer/Database.cs index 87dd47ae..8669fd84 100644 --- a/grate.unittests/SqlServer/Database.cs +++ b/grate.unittests/SqlServer/Database.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; using grate.unittests.TestInfrastructure; using NUnit.Framework; @@ -8,5 +11,22 @@ namespace grate.unittests.SqlServer; public class Database: Generic.GenericDatabase { protected override IGrateTestContext Context => GrateTestContext.SqlServer; - -} \ No newline at end of file + + [Test] + public async Task Does_not_needlessly_apply_case_sensitive_database_name_checks_Issue_167() + { + // There's a bug where if the database name specified by the user differs from the actual database only by case then + // Grate currently attempts to create the database again, only for it to fail on the DBMS (Sql Server, case insensitive bug only). + + var db = "CASEDATABASE"; + await CreateDatabase(db); + + // Check that the database has been created + IEnumerable databasesBeforeMigration = await GetDatabases(); + databasesBeforeMigration.Should().Contain(db); + + await using var migrator = GetMigrator(GetConfiguration(db.ToLower(), true)); // ToLower is important here, this reproduces the bug in #167 + // There should be no errors running the migration + Assert.DoesNotThrowAsync(() => migrator.Migrate()); + } +} diff --git a/grate.unittests/SqlServer/MigrationTables.cs b/grate.unittests/SqlServer/MigrationTables.cs index 28db250a..aaf55daf 100644 --- a/grate.unittests/SqlServer/MigrationTables.cs +++ b/grate.unittests/SqlServer/MigrationTables.cs @@ -8,4 +8,14 @@ namespace grate.unittests.SqlServer; public class MigrationTables: Generic.GenericMigrationTables { protected override IGrateTestContext Context => GrateTestContext.SqlServer; -} \ No newline at end of file + + protected override string CountTableSql(string schemaName, string tableName) + { + return $@" +SELECT count(table_name) FROM INFORMATION_SCHEMA.TABLES +WHERE +TABLE_SCHEMA = '{schemaName}' AND +TABLE_NAME = '{tableName}' COLLATE Latin1_General_CS_AS +"; + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/Database.cs b/grate.unittests/SqlServerCaseSensitive/Database.cs new file mode 100644 index 00000000..b211b35d --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/Database.cs @@ -0,0 +1,13 @@ +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.SqlServerCaseSensitive +{ + [TestFixture] + [Category("SqlServerCaseSensitive")] + public class Database : Generic.GenericDatabase + { + protected override IGrateTestContext Context => GrateTestContext.SqlServerCaseSensitive; + + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/DockerContainer.cs b/grate.unittests/SqlServerCaseSensitive/DockerContainer.cs new file mode 100644 index 00000000..7e1766ba --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/DockerContainer.cs @@ -0,0 +1,12 @@ +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.SqlServerCaseSensitive +{ + [TestFixture] + [Category("SqlServerCaseSensitive")] + public class DockerContainer : Generic.GenericDockerContainer + { + protected override IGrateTestContext Context => GrateTestContext.SqlServerCaseSensitive; + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/MigrationTables.cs b/grate.unittests/SqlServerCaseSensitive/MigrationTables.cs new file mode 100644 index 00000000..a7d1aefe --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/MigrationTables.cs @@ -0,0 +1,12 @@ +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.SqlServerCaseSensitive +{ + [TestFixture] + [Category("SqlServerCaseSensitive")] + public class MigrationTables : Generic.GenericMigrationTables + { + protected override IGrateTestContext Context => GrateTestContext.SqlServerCaseSensitive; + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Anytime_scripts.cs b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Anytime_scripts.cs new file mode 100644 index 00000000..383dedaf --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Anytime_scripts.cs @@ -0,0 +1,13 @@ +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.SqlServerCaseSensitive.Running_MigrationScripts +{ + [TestFixture] + [Category("SqlServerCaseSensitive")] + // ReSharper disable once InconsistentNaming + public class Anytime_scripts : Generic.Running_MigrationScripts.Anytime_scripts + { + protected override IGrateTestContext Context => GrateTestContext.SqlServerCaseSensitive; + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/DropDatabase.cs b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/DropDatabase.cs new file mode 100644 index 00000000..2da38f27 --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/DropDatabase.cs @@ -0,0 +1,12 @@ +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.SqlServerCaseSensitive.Running_MigrationScripts +{ + [TestFixture] + [Category("SqlServerCaseSensitive")] + public class DropDatabase : Generic.Running_MigrationScripts.DropDatabase + { + protected override IGrateTestContext Context => GrateTestContext.SqlServerCaseSensitive; + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Environment_scripts.cs b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Environment_scripts.cs new file mode 100644 index 00000000..589ccd43 --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Environment_scripts.cs @@ -0,0 +1,13 @@ +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.SqlServerCaseSensitive.Running_MigrationScripts +{ + [TestFixture] + [Category("SqlServerCaseSensitive")] + // ReSharper disable once InconsistentNaming + public class Environment_scripts : Generic.Running_MigrationScripts.Environment_scripts + { + protected override IGrateTestContext Context => GrateTestContext.SqlServerCaseSensitive; + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Everytime_scripts.cs b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Everytime_scripts.cs new file mode 100644 index 00000000..c31af5ae --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Everytime_scripts.cs @@ -0,0 +1,13 @@ +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.SqlServerCaseSensitive.Running_MigrationScripts +{ + [TestFixture] + [Category("SqlServerCaseSensitive")] + // ReSharper disable once InconsistentNaming + public class Everytime_scripts : Generic.Running_MigrationScripts.Everytime_scripts + { + protected override IGrateTestContext Context => GrateTestContext.SqlServerCaseSensitive; + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Failing_Scripts.cs b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Failing_Scripts.cs new file mode 100644 index 00000000..481bcf6b --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Failing_Scripts.cs @@ -0,0 +1,14 @@ +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.SqlServerCaseSensitive.Running_MigrationScripts +{ + [TestFixture] + [Category("SqlServerCaseSensitive")] + // ReSharper disable once InconsistentNaming + public class Failing_Scripts : Generic.Running_MigrationScripts.Failing_Scripts + { + protected override IGrateTestContext Context => GrateTestContext.SqlServerCaseSensitive; + protected override string ExpectedErrorMessageForInvalidSql => "Incorrect syntax near 'TOP'."; + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/One_time_scripts.cs b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/One_time_scripts.cs new file mode 100644 index 00000000..2e78bab1 --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/One_time_scripts.cs @@ -0,0 +1,13 @@ +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.SqlServerCaseSensitive.Running_MigrationScripts +{ + [TestFixture] + [Category("SqlServerCaseSensitive")] + // ReSharper disable once InconsistentNaming + public class One_time_scripts : Generic.Running_MigrationScripts.One_time_scripts + { + protected override IGrateTestContext Context => GrateTestContext.SqlServerCaseSensitive; + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Order_Of_Scripts.cs b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Order_Of_Scripts.cs new file mode 100644 index 00000000..9b7a57b0 --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Order_Of_Scripts.cs @@ -0,0 +1,13 @@ +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.SqlServerCaseSensitive.Running_MigrationScripts +{ + [TestFixture] + [Category("SqlServerCaseSensitive")] + // ReSharper disable once InconsistentNaming + public class Order_Of_Scripts : Generic.Running_MigrationScripts.Order_Of_Scripts + { + protected override IGrateTestContext Context => GrateTestContext.SqlServerCaseSensitive; + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/RestoreDatabase.cs b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/RestoreDatabase.cs new file mode 100644 index 00000000..b6252746 --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/RestoreDatabase.cs @@ -0,0 +1,60 @@ +using System.Linq; +using System.Threading.Tasks; +using Dapper; +using FluentAssertions; +using grate.Configuration; +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.SqlServerCaseSensitive.Running_MigrationScripts +{ + [TestFixture] + [Category("SqlServerCaseSensitive")] + public class RestoreDatabase : SqlServerScriptsBase + { + protected override IGrateTestContext Context => GrateTestContext.SqlServerCaseSensitive; + private readonly string _backupPath = "/var/opt/mssql/backup/test.bak"; + + [OneTimeSetUp] + public async Task RunBeforeTest() + { + await using (var conn = Context.CreateDbConnection("master")) + { + await conn.ExecuteAsync("use [master] CREATE DATABASE [test]"); + await conn.ExecuteAsync("use [test] CREATE TABLE dbo.Table_1 (column1 int NULL)"); + await conn.ExecuteAsync($"BACKUP DATABASE [test] TO DISK = '{_backupPath}'"); + await conn.ExecuteAsync("use [master] DROP DATABASE [test]"); + } + } + + [Test] + public async Task Ensure_database_gets_restored() + { + var db = TestConfig.RandomDatabase(); + + var parent = CreateRandomTempDirectory(); + var knownFolders = FoldersConfiguration.Default(null); + CreateDummySql(parent, knownFolders[KnownFolderKeys.Sprocs]); + + var restoreConfig = Context.GetConfiguration(db, parent, knownFolders) with + { + Restore = _backupPath + }; + + await using (var migrator = Context.GetMigrator(restoreConfig)) + { + await migrator.Migrate(); + } + + int[] results; + string sql = $"select count(1) from sys.tables where [name]='Table_1'"; + + await using (var conn = Context.CreateDbConnection(db)) + { + results = (await conn.QueryAsync(sql)).ToArray(); + } + + results.First().Should().Be(1); + } + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/ScriptsRun_Table.cs b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/ScriptsRun_Table.cs new file mode 100644 index 00000000..c5c5453b --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/ScriptsRun_Table.cs @@ -0,0 +1,9 @@ +using grate.unittests.TestInfrastructure; + +namespace grate.unittests.SqlServerCaseSensitive.Running_MigrationScripts +{ + public class ScriptsRun_Table : Generic.Running_MigrationScripts.ScriptsRun_Table + { + protected override IGrateTestContext Context => GrateTestContext.SqlServerCaseSensitive; + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/SqlServerScriptsBase.cs b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/SqlServerScriptsBase.cs new file mode 100644 index 00000000..7c81a13f --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/SqlServerScriptsBase.cs @@ -0,0 +1,8 @@ +using grate.unittests.Generic.Running_MigrationScripts; + +namespace grate.unittests.SqlServerCaseSensitive.Running_MigrationScripts +{ + public abstract class SqlServerScriptsBase : MigrationsScriptsBase + { + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/TokenScripts.cs b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/TokenScripts.cs new file mode 100644 index 00000000..c89d8015 --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/TokenScripts.cs @@ -0,0 +1,12 @@ +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.SqlServerCaseSensitive.Running_MigrationScripts +{ + [TestFixture] + [Category("SqlServerCaseSensitive")] + public class TokenScripts : Generic.Running_MigrationScripts.TokenScripts + { + protected override IGrateTestContext Context => GrateTestContext.SqlServerCaseSensitive; + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Versioning_The_Database.cs b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Versioning_The_Database.cs new file mode 100644 index 00000000..ac04af59 --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/Running_MigrationScripts/Versioning_The_Database.cs @@ -0,0 +1,13 @@ +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.SqlServerCaseSensitive.Running_MigrationScripts +{ + [TestFixture] + [Category("SqlServerCaseSensitive")] + // ReSharper disable once InconsistentNaming + public class Versioning_The_Database : Generic.Running_MigrationScripts.Versioning_The_Database + { + protected override IGrateTestContext Context => GrateTestContext.SqlServerCaseSensitive; + } +} diff --git a/grate.unittests/SqlServerCaseSensitive/SetupTestEnvironment.cs b/grate.unittests/SqlServerCaseSensitive/SetupTestEnvironment.cs new file mode 100644 index 00000000..75fd4dfc --- /dev/null +++ b/grate.unittests/SqlServerCaseSensitive/SetupTestEnvironment.cs @@ -0,0 +1,13 @@ +using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +namespace grate.unittests.SqlServerCaseSensitive +{ + [SetUpFixture] + [Category("SqlServerCaseSensitive")] + public class SetupTestEnvironment : Generic.SetupDockerTestEnvironment + { + protected override IGrateTestContext GrateTestContext => unittests.GrateTestContext.SqlServerCaseSensitive; + protected override IDockerTestContext DockerTestContext => unittests.GrateTestContext.SqlServerCaseSensitive; + } +} diff --git a/grate.unittests/TestContext.cs b/grate.unittests/TestContext.cs index 43f03bf9..5e75cb7b 100644 --- a/grate.unittests/TestContext.cs +++ b/grate.unittests/TestContext.cs @@ -1,10 +1,15 @@ using grate.unittests.TestInfrastructure; +using NUnit.Framework; + +// There are some parallelism issues, but this does not solve it +//[assembly:LevelOfParallelism(1)] namespace grate.unittests; public static class GrateTestContext { internal static readonly SqlServerGrateTestContext SqlServer = new(); + internal static readonly SqlServerGrateTestContext SqlServerCaseSensitive = new("Latin1_General_CS_AS"); //CS == Case Sensitive internal static readonly OracleGrateTestContext Oracle = new(); internal static readonly PostgreSqlGrateTestContext PostgreSql = new(); // ReSharper disable once InconsistentNaming diff --git a/grate.unittests/TestInfrastructure/Docker.cs b/grate.unittests/TestInfrastructure/Docker.cs index 71c8487e..701c66fa 100644 --- a/grate.unittests/TestInfrastructure/Docker.cs +++ b/grate.unittests/TestInfrastructure/Docker.cs @@ -23,7 +23,14 @@ public static class Docker //TestContext.Progress.WriteLine("find port: " + findPortArgs); var hostPortList = await RunDockerCommand(findPortArgs); - var hostPort = hostPortList.Split(" ", StringSplitOptions.RemoveEmptyEntries).First(); + var hostPort = hostPortList.Split(" ", StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + + if (hostPort is null) + { + throw new TestInfrastructureSetupException( + $"Unable to get host port from docker run output '${hostPortList}'"); + } + return (serverName, int.Parse(hostPort)); } diff --git a/grate.unittests/TestInfrastructure/IGrateTestContext.cs b/grate.unittests/TestInfrastructure/IGrateTestContext.cs index 1756a8c0..dd8d72b1 100644 --- a/grate.unittests/TestInfrastructure/IGrateTestContext.cs +++ b/grate.unittests/TestInfrastructure/IGrateTestContext.cs @@ -56,6 +56,17 @@ DefaultConfiguration with SqlFilesDirectory = sqlFilesDirectory }; + public GrateConfiguration GetConfiguration(string databaseName, DirectoryInfo sqlFilesDirectory, + IFoldersConfiguration knownFolders, string? env, bool runInTransaction) => + DefaultConfiguration with + { + ConnectionString = ConnectionString(databaseName), + Folders = knownFolders, + Environment = env != null ? new GrateEnvironment(env) : null, + Transaction = runInTransaction, + SqlFilesDirectory = sqlFilesDirectory + }; + public GrateMigrator GetMigrator(GrateConfiguration config) { var factory = Substitute.For(); diff --git a/grate.unittests/TestInfrastructure/NUnitLogger.cs b/grate.unittests/TestInfrastructure/NUnitLogger.cs index d472355c..b9a63e90 100644 --- a/grate.unittests/TestInfrastructure/NUnitLogger.cs +++ b/grate.unittests/TestInfrastructure/NUnitLogger.cs @@ -20,7 +20,7 @@ public NUnitLogger(string name, LogLevel minimumLogLevel) _minimumLogLevel = minimumLogLevel; } - public IDisposable BeginScope(TState state) => default; //We don't have scoping support + public IDisposable BeginScope(TState state) where TState : notnull => default; //We don't have scoping support public bool IsEnabled(LogLevel logLevel) => _minimumLogLevel >= logLevel ; diff --git a/grate.unittests/TestInfrastructure/OracleGrateTestContext.cs b/grate.unittests/TestInfrastructure/OracleGrateTestContext.cs index ec555716..064f72c0 100644 --- a/grate.unittests/TestInfrastructure/OracleGrateTestContext.cs +++ b/grate.unittests/TestInfrastructure/OracleGrateTestContext.cs @@ -15,12 +15,12 @@ internal class OracleGrateTestContext : TestContextBase, IGrateTestContext, IDoc public override int? ContainerPort => 1521; public string DockerCommand(string serverName, string adminPassword) => - $"run -d --name {serverName} -e ORACLE_ENABLE_XDB=true -P oracleinanutshell/oracle-xe-11g:latest"; + $"run -d --name {serverName} -p 1521 -e ORACLE_ENABLE_XDB=true -e ORACLE_PWD={adminPassword} -P container-registry.oracle.com/database/express:21.3.0-xe"; - public string AdminConnectionString => $@"Data Source=localhost:{Port}/XE;User ID=SYSTEM;Password=oracle;Pooling=False"; - public string ConnectionString(string database) => $@"Data Source=localhost:{Port}/XE;User ID={database.ToUpper()};Password=oracle;Pooling=False"; - public string UserConnectionString(string database) => $@"Data Source=localhost:{Port}/XE;User ID={database.ToUpper()};Password=oracle;Pooling=False"; + public string AdminConnectionString => $@"Data Source=localhost:{Port}/XEPDB1;User ID=system;Password={AdminPassword};Pooling=False"; + public string ConnectionString(string database) => $@"Data Source=localhost:{Port}/XEPDB1;User ID={database.ToUpper()};Password={AdminPassword};Pooling=False"; + public string UserConnectionString(string database) => $@"Data Source=localhost:{Port}/XEPDB1;User ID={database.ToUpper()};Password={AdminPassword};Pooling=False"; public DbConnection GetDbConnection(string connectionString) => new OracleConnection(connectionString); @@ -29,6 +29,7 @@ public string DockerCommand(string serverName, string adminPassword) => public DatabaseType DatabaseType => DatabaseType.oracle; public bool SupportsTransaction => false; + public string DatabaseTypeName => "Oracle"; public string MasterDatabase => "oracle"; @@ -40,6 +41,6 @@ public string DockerCommand(string serverName, string adminPassword) => SleepTwoSeconds = "sys.dbms_session.sleep(2);" }; - public string ExpectedVersionPrefix => "Oracle Database 11g Express Edition Release 11.2.0.2.0 - 64bit Production"; + public string ExpectedVersionPrefix => "Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production"; public bool SupportsCreateDatabase => true; } diff --git a/grate.unittests/TestInfrastructure/OracleSplitterContext.cs b/grate.unittests/TestInfrastructure/OracleSplitterContext.cs new file mode 100644 index 00000000..5c5e856d --- /dev/null +++ b/grate.unittests/TestInfrastructure/OracleSplitterContext.cs @@ -0,0 +1,221 @@ +using grate.Infrastructure; +// ReSharper disable StringLiteralTypo + +namespace grate.unittests.TestInfrastructure; + +public static class OracleSplitterContext +{ + + public static class FullSplitter + { + public static string PLSqlStatement = @" +BOB1 +/ + +/* COMMENT */ +BOB2 +/ + +-- / + +BOB3 / + +--`~!@#$%^&*()-_+=,.;:'""[]\/?<> / + +BOB5 + / + +BOB6 +/ + +/* / */ + +BOB7 + +/* + +/ + +*/ + +BOB8 + +-- +/ + +BOB9 + +-- `~!@#$%^&*()-_+=,.;:'""[]\/?<> +/ + +BOB10/ + +CREATE TABLE PO/ +{} + +INSERT INTO PO/ (id,desc) VALUES (1,'/') + +BOB11 + + -- TODO: To be good, there should be type column + +-- dfgjhdfgdjkgk dfgdfg / +BOB12 + +UPDATE Timmy SET id = 'something something /' +UPDATE Timmy SET id = 'something something: /' + +ALTER TABLE Inv.something ADD + gagagag decimal(20, 12) NULL, + asdfasdf DECIMAL(20, 6) NULL, + didbibi decimal(20, 6) NULL, + yeppsasd decimal(20, 6) NULL, + uhuhhh datetime NULL, + slsald varchar(15) NULL, + uhasdf varchar(15) NULL, + daf_asdfasdf DECIMAL(20,6) NULL; +/ + +EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Daily job', + @step_id=1, + @cmdexec_success_code=0, + @on_success_action=3, + @on_success_step_id=0, + @on_fail_action=3, + @on_fail_step_id=0, + @retry_attempts=0, + @retry_interval=0, + @os_run_priority=0, @subsystem=N'PLSQL', + @command=N' +dml statements +/ +dml statements ' + +/ + +INSERT [dbo].[Foo] ([Bar]) VALUES (N'hello--world. +Thanks!') +INSERT [dbo].[Foo] ([Bar]) VALUES (N'/ speed racer, / speed racer, / speed racer /!!!!! ') + +/"; + + public static string PLSqlStatementScrubbed = @" +BOB1 +" + StatementSplitter.BatchTerminatorReplacementString + @" + +/* COMMENT */ +BOB2 +" + StatementSplitter.BatchTerminatorReplacementString + @" + +-- / + +BOB3 " + StatementSplitter.BatchTerminatorReplacementString + @" + +--`~!@#$%^&*()-_+=,.;:'""[]\/?<> / + +BOB5 + " + StatementSplitter.BatchTerminatorReplacementString + @" + +BOB6 +" + StatementSplitter.BatchTerminatorReplacementString + @" + +/* / */ + +BOB7 + +/* + +/ + +*/ + +BOB8 + +-- +" + StatementSplitter.BatchTerminatorReplacementString + @" + +BOB9 + +-- `~!@#$%^&*()-_+=,.;:'""[]\/?<> +" + StatementSplitter.BatchTerminatorReplacementString + @" + +BOB10/ + +CREATE TABLE PO/ +{} + +INSERT INTO PO/ (id,desc) VALUES (1,'/') + +BOB11 + + -- TODO: To be good, there should be type column + +-- dfgjhdfgdjkgk dfgdfg / +BOB12 + +UPDATE Timmy SET id = 'something something /' +UPDATE Timmy SET id = 'something something: /' + +ALTER TABLE Inv.something ADD + gagagag decimal(20, 12) NULL, + asdfasdf DECIMAL(20, 6) NULL, + didbibi decimal(20, 6) NULL, + yeppsasd decimal(20, 6) NULL, + uhuhhh datetime NULL, + slsald varchar(15) NULL, + uhasdf varchar(15) NULL, + daf_asdfasdf DECIMAL(20,6) NULL; +" + StatementSplitter.BatchTerminatorReplacementString + @" + +EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Daily job', + @step_id=1, + @cmdexec_success_code=0, + @on_success_action=3, + @on_success_step_id=0, + @on_fail_action=3, + @on_fail_step_id=0, + @retry_attempts=0, + @retry_interval=0, + @os_run_priority=0, @subsystem=N'PLSQL', + @command=N' +dml statements +/ +dml statements ' + +" + StatementSplitter.BatchTerminatorReplacementString + @" + +INSERT [dbo].[Foo] ([Bar]) VALUES (N'hello--world. +Thanks!') +INSERT [dbo].[Foo] ([Bar]) VALUES (N'/ speed racer, / speed racer, / speed racer /!!!!! ') + +" + StatementSplitter.BatchTerminatorReplacementString + @""; + + public static string plsql_statement = + @" +SQL1; +; +SQL2; +; +tmpSql := 'DROP SEQUENCE mutatieStockID'; +EXECUTE IMMEDIATE tmpSql; +; +BEGIN +INSERT into Table (columnname) values ("";""); +UPDATE Table set columnname="";""; +END; +"; + public static string plsql_statement_scrubbed = @" +SQL1; +" + StatementSplitter.BatchTerminatorReplacementString + @" +SQL2; +" + StatementSplitter.BatchTerminatorReplacementString + @" +tmpSql := 'DROP SEQUENCE mutatieStockID'; +EXECUTE IMMEDIATE tmpSql; +" + StatementSplitter.BatchTerminatorReplacementString + @" +BEGIN +INSERT into Table (columnname) values ("";""); +UPDATE Table set columnname="";""; +END; +"; + } +} diff --git a/grate.unittests/TestInfrastructure/PostgreSqlGrateTestContext.cs b/grate.unittests/TestInfrastructure/PostgreSqlGrateTestContext.cs index 561684e4..4db5fb13 100644 --- a/grate.unittests/TestInfrastructure/PostgreSqlGrateTestContext.cs +++ b/grate.unittests/TestInfrastructure/PostgreSqlGrateTestContext.cs @@ -39,6 +39,6 @@ public string DockerCommand(string serverName, string adminPassword) => }; - public string ExpectedVersionPrefix => "PostgreSQL 14."; + public string ExpectedVersionPrefix => "PostgreSQL 15."; public bool SupportsCreateDatabase => true; } diff --git a/grate.unittests/TestInfrastructure/SqlServerGrateTestContext.cs b/grate.unittests/TestInfrastructure/SqlServerGrateTestContext.cs index 518c102e..f204bc45 100644 --- a/grate.unittests/TestInfrastructure/SqlServerGrateTestContext.cs +++ b/grate.unittests/TestInfrastructure/SqlServerGrateTestContext.cs @@ -12,6 +12,13 @@ namespace grate.unittests.TestInfrastructure; class SqlServerGrateTestContext : TestContextBase, IGrateTestContext, IDockerTestContext { + + + public SqlServerGrateTestContext(string serverCollation) => ServerCollation = serverCollation; + + public SqlServerGrateTestContext(): this("Danish_Norwegian_CI_AS") + {} + public string AdminPassword { get; set; } = default!; public int? Port { get; set; } public override int? ContainerPort => 1433; @@ -25,7 +32,7 @@ class SqlServerGrateTestContext : TestContextBase, IGrateTestContext, IDockerTes }; public string DockerCommand(string serverName, string adminPassword) => - $"run -d --name {serverName} -e ACCEPT_EULA=Y -e SA_PASSWORD={adminPassword} -e MSSQL_PID=Developer -e MSSQL_COLLATION=Danish_Norwegian_CI_AS -P {DockerImage}"; + $"run -d --name {serverName} -e ACCEPT_EULA=Y -e SA_PASSWORD={adminPassword} -e MSSQL_PID=Developer -e MSSQL_COLLATION={ServerCollation} -P {DockerImage}"; public string AdminConnectionString => $"Data Source=localhost,{Port};Initial Catalog=master;User Id=sa;Password={AdminPassword};Encrypt=false;Pooling=false"; public string ConnectionString(string database) => $"Data Source=localhost,{Port};Initial Catalog={database};User Id=sa;Password={AdminPassword};Encrypt=false;Pooling=false"; @@ -57,4 +64,6 @@ public string DockerCommand(string serverName, string adminPassword) => }; public bool SupportsCreateDatabase => true; + + public string ServerCollation { get; } } diff --git a/grate.unittests/TestInfrastructure/SplitterContext.cs b/grate.unittests/TestInfrastructure/SqlServerSplitterContext.cs similarity index 98% rename from grate.unittests/TestInfrastructure/SplitterContext.cs rename to grate.unittests/TestInfrastructure/SqlServerSplitterContext.cs index 3a0acd47..b897ae25 100644 --- a/grate.unittests/TestInfrastructure/SplitterContext.cs +++ b/grate.unittests/TestInfrastructure/SqlServerSplitterContext.cs @@ -3,7 +3,7 @@ namespace grate.unittests.TestInfrastructure; -public static class SplitterContext +public static class SqlServerSplitterContext { public static class FullSplitter diff --git a/grate.unittests/TestInfrastructure/SqlStatements.cs b/grate.unittests/TestInfrastructure/SqlStatements.cs index f5716658..c36c1967 100644 --- a/grate.unittests/TestInfrastructure/SqlStatements.cs +++ b/grate.unittests/TestInfrastructure/SqlStatements.cs @@ -6,4 +6,5 @@ public record SqlStatements public string SleepTwoSeconds { get; init; } = default!; public string CreateUser { get; init; } = default!; public string GrantAccess { get; init; } = default!; + public string LineComment { get; init; } = "--"; } diff --git a/grate.unittests/TestInfrastructure/TestConfig.cs b/grate.unittests/TestInfrastructure/TestConfig.cs index c7b76cc1..efa3aaba 100644 --- a/grate.unittests/TestInfrastructure/TestConfig.cs +++ b/grate.unittests/TestInfrastructure/TestConfig.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using grate.Configuration; using Microsoft.Extensions.Logging; using static System.StringSplitOptions; @@ -67,5 +68,15 @@ public static DirectoryInfo MakeSurePathExists(DirectoryInfo? path) return path; } - + + public static IGrateTestContext GetTestContext(DatabaseType databaseType) => databaseType switch + { + DatabaseType.mariadb => new MariaDbGrateTestContext(), + DatabaseType.oracle => new OracleGrateTestContext(), + DatabaseType.postgresql => new PostgreSqlGrateTestContext(), + DatabaseType.sqlite => new SqliteGrateTestContext(), + DatabaseType.sqlserver => new SqlServerGrateTestContext(), + _ => throw new ArgumentOutOfRangeException(nameof(databaseType), databaseType.ToString()) + }; + } diff --git a/grate.unittests/TestInfrastructure/TestInfrastructureSetupException.cs b/grate.unittests/TestInfrastructure/TestInfrastructureSetupException.cs new file mode 100644 index 00000000..e5cdf371 --- /dev/null +++ b/grate.unittests/TestInfrastructure/TestInfrastructureSetupException.cs @@ -0,0 +1,10 @@ +using System; + +namespace grate.unittests.TestInfrastructure; + +public class TestInfrastructureSetupException: Exception +{ + public TestInfrastructureSetupException(string message): base(message) + { + } +} diff --git a/grate.unittests/grate.unittests.csproj b/grate.unittests/grate.unittests.csproj index c03b80fd..dcf3d584 100644 --- a/grate.unittests/grate.unittests.csproj +++ b/grate.unittests/grate.unittests.csproj @@ -1,26 +1,26 @@ - net6.0 + net7.0 false enable - + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + - - + + diff --git a/grate/Commands/MigrateCommand.cs b/grate/Commands/MigrateCommand.cs index adeea2c6..20d4c368 100644 --- a/grate/Commands/MigrateCommand.cs +++ b/grate/Commands/MigrateCommand.cs @@ -41,6 +41,7 @@ public MigrateCommand(GrateMigrator mi) : base("Migrates the database") Add(RunAllAnyTimeScripts()); Add(DryRun()); Add(Restore()); + Add(IgnoreDirectoryNames()); Handler = CommandHandler.Create( async () => @@ -96,7 +97,7 @@ private static Option Folders() => Example: - --folders up=ddl;views=projections;beforemigration=preparefordeploy + --folders 'up=ddl;views=projections;beforemigration=preparefordeploy' or @@ -300,4 +301,10 @@ private static Option ServerName() => //() => DefaultServerName, "OBSOLETE: Please specify the connection string instead." ); + + private static Option IgnoreDirectoryNames() => + new( + new[] { "--ignoredirectorynames", "--searchallinsteadoftraverse", "--searchallsubdirectoriesinsteadoftraverse" }, + "IgnoreDirectoryNames - By default, scripts are ordered by relative path including subdirectories. This option searches subdirectories, but order is based on filename alone." + ); } diff --git a/grate/Configuration/FoldersConfiguration.cs b/grate/Configuration/FoldersConfiguration.cs index 8529d957..d6729719 100644 --- a/grate/Configuration/FoldersConfiguration.cs +++ b/grate/Configuration/FoldersConfiguration.cs @@ -23,15 +23,17 @@ public FoldersConfiguration(IDictionary source) public FoldersConfiguration() { } - + + public MigrationsFolder? CreateDatabase { get; set; } + public MigrationsFolder? DropDatabase { get; set; } public static FoldersConfiguration Empty => new(); public static IFoldersConfiguration Default(IKnownFolderNames? folderNames = null) { folderNames ??= KnownFolderNames.Default; - - return new FoldersConfiguration() + + var foldersConfiguration = new FoldersConfiguration() { { KnownFolderKeys.BeforeMigration, new MigrationsFolder("BeforeMigration", folderNames.BeforeMigration, EveryTime, TransactionHandling: TransactionHandling.Autonomous) }, { KnownFolderKeys.AlterDatabase , new MigrationsFolder("AlterDatabase", folderNames.AlterDatabase, AnyTime, ConnectionType.Admin, TransactionHandling.Autonomous) }, @@ -46,8 +48,12 @@ public static IFoldersConfiguration Default(IKnownFolderNames? folderNames = nul { KnownFolderKeys.Indexes, new MigrationsFolder("Indexes", folderNames.Indexes, AnyTime) }, { KnownFolderKeys.RunAfterOtherAnyTimeScripts, new MigrationsFolder("Run after Other Anytime Scripts", folderNames.RunAfterOtherAnyTimeScripts, AnyTime) }, { KnownFolderKeys.Permissions, new MigrationsFolder("Permissions", folderNames.Permissions, EveryTime, TransactionHandling: TransactionHandling.Autonomous) }, - { KnownFolderKeys.AfterMigration, new MigrationsFolder("AfterMigration", folderNames.AfterMigration, EveryTime, TransactionHandling: TransactionHandling.Autonomous) } + { KnownFolderKeys.AfterMigration, new MigrationsFolder("AfterMigration", folderNames.AfterMigration, EveryTime, TransactionHandling: TransactionHandling.Autonomous) }, }; + foldersConfiguration.CreateDatabase = new MigrationsFolder("CreateDatabase", folderNames.CreateDatabase, AnyTime, ConnectionType.Admin, TransactionHandling.Autonomous); + foldersConfiguration.DropDatabase = new MigrationsFolder("DropDatabase", folderNames.DropDatabase, AnyTime, ConnectionType.Admin, TransactionHandling.Autonomous); + + return foldersConfiguration; } } diff --git a/grate/Configuration/GrateConfiguration.cs b/grate/Configuration/GrateConfiguration.cs index fa049038..fd646cc4 100644 --- a/grate/Configuration/GrateConfiguration.cs +++ b/grate/Configuration/GrateConfiguration.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using grate.Infrastructure; @@ -25,6 +26,10 @@ public record GrateConfiguration public string? ConnectionString { get; init; } = null; public string SchemaName { get; init; } = "grate"; + + public string ScriptsRunTableName { get; set; } = "ScriptsRun"; + public string ScriptsRunErrorsTableName { get; set; } = "ScriptsRunErrors"; + public string VersionTableName { get; set; } = "Version"; public string? AdminConnectionString { @@ -34,14 +39,15 @@ public string? AdminConnectionString public string? AccessToken { get; set; } = null; - private static string? WithAdminDb(string? connectionString) + private string? WithAdminDb(string? connectionString) { if (string.IsNullOrEmpty(connectionString)) { return connectionString; } var pattern = new Regex("(.*;\\s*(?:Initial Catalog|Database)=)([^;]*)(.*)"); - var replaced = pattern.Replace(connectionString, "$1master$3"); + var replacement = $"$1{GetMasterDbName(DatabaseType)}$3"; + var replaced = pattern.Replace(connectionString, replacement); return replaced; } @@ -119,4 +125,20 @@ public string? AdminConnectionString /// If specified, location of the backup file to use when restoring /// public string? Restore { get; init; } + + /// + /// By default, scripts are ordered by relative path including subdirectories. This option searches subdirectories, but order is based on filename alone. + /// + public bool IgnoreDirectoryNames { get; set; } + + private static string GetMasterDbName(DatabaseType databaseType) => databaseType switch + { + DatabaseType.mariadb => "mysql", + DatabaseType.oracle => "oracle", + DatabaseType.postgresql => "postgres", + DatabaseType.sqlite => "master", + DatabaseType.sqlserver => "master", + _ => throw new ArgumentOutOfRangeException(nameof(databaseType), databaseType.ToString()) + }; + } diff --git a/grate/Configuration/IFoldersConfiguration.cs b/grate/Configuration/IFoldersConfiguration.cs index 7e88e0c9..4fda0bc4 100644 --- a/grate/Configuration/IFoldersConfiguration.cs +++ b/grate/Configuration/IFoldersConfiguration.cs @@ -4,4 +4,6 @@ namespace grate.Configuration; public interface IFoldersConfiguration: IDictionary { + MigrationsFolder? CreateDatabase { get; set; } + MigrationsFolder? DropDatabase { get; set; } } diff --git a/grate/Configuration/IKnownFolderNames.cs b/grate/Configuration/IKnownFolderNames.cs index c4fe97d0..dc67d7d9 100644 --- a/grate/Configuration/IKnownFolderNames.cs +++ b/grate/Configuration/IKnownFolderNames.cs @@ -3,6 +3,8 @@ public interface IKnownFolderNames { string BeforeMigration { get; } + string CreateDatabase { get; } + string DropDatabase { get; } string AlterDatabase { get; } string RunAfterCreateDatabase { get; } string RunBeforeUp { get; } diff --git a/grate/Configuration/KnownFolderKeys.cs b/grate/Configuration/KnownFolderKeys.cs index d227c6cb..58def349 100644 --- a/grate/Configuration/KnownFolderKeys.cs +++ b/grate/Configuration/KnownFolderKeys.cs @@ -5,6 +5,7 @@ namespace grate.Configuration; public static class KnownFolderKeys { public const string BeforeMigration = nameof(BeforeMigration); + public const string CreateDatabase = nameof(CreateDatabase); public const string AlterDatabase = nameof(AlterDatabase); public const string RunAfterCreateDatabase = nameof(RunAfterCreateDatabase); public const string RunBeforeUp = nameof(RunBeforeUp); diff --git a/grate/Configuration/KnownFolderNames.cs b/grate/Configuration/KnownFolderNames.cs index 507fafea..b05a1c4e 100644 --- a/grate/Configuration/KnownFolderNames.cs +++ b/grate/Configuration/KnownFolderNames.cs @@ -3,6 +3,8 @@ public record KnownFolderNames: IKnownFolderNames { public string BeforeMigration { get; init; } = "beforeMigration"; + public string CreateDatabase { get; init; } = "createDatabase"; + public string DropDatabase { get; init; } = "dropDatabase"; public string AlterDatabase { get; init; } = "alterDatabase"; public string RunAfterCreateDatabase { get; init; } = "runAfterCreateDatabase"; public string RunBeforeUp { get; init; } = "runBeforeUp"; diff --git a/grate/Infrastructure/GrateConsoleFormatter.cs b/grate/Infrastructure/GrateConsoleFormatter.cs index 780771a7..43628339 100644 --- a/grate/Infrastructure/GrateConsoleFormatter.cs +++ b/grate/Infrastructure/GrateConsoleFormatter.cs @@ -21,7 +21,7 @@ public GrateConsoleFormatter(IOptionsMonitor? opt } } - public override void Write(in LogEntry logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter) + public override void Write(in LogEntry logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter) { string? message = logEntry.Formatter?.Invoke(logEntry.State, logEntry.Exception); if (logEntry.Exception == null && message == null) @@ -52,11 +52,11 @@ private static void CreateDefaultLogMessage(TextWriter textWriter, in Lo LogLevel logLevel = logEntry.LogLevel; ConsoleColors logLevelColors = GetLogLevelConsoleColors(logLevel); - textWriter.WriteColoredMessageLine(message, logLevelColors.Background, logLevelColors.Foreground); + textWriter.WriteColoredMessageLine(message, logLevelColors.Foreground); if (exception != null) { - textWriter.WriteColoredMessageLine(exception.ToString(), logLevelColors.Background, logLevelColors.Foreground); + textWriter.WriteColoredMessageLine(exception.ToString(), logLevelColors.Foreground); } textWriter.Flush(); } @@ -74,12 +74,12 @@ private static ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel) // since just setting one can look bad on the users console. return logLevel switch { - LogLevel.Trace => new ConsoleColors(GrateConsoleColor.Foreground.DarkYellow, GrateConsoleColor.Background.Black), - LogLevel.Debug => new ConsoleColors(GrateConsoleColor.Foreground.DarkGray, GrateConsoleColor.Background.Black), - LogLevel.Information => new ConsoleColors(GrateConsoleColor.Foreground.Green, GrateConsoleColor.Background.Black), - LogLevel.Warning => new ConsoleColors(GrateConsoleColor.Foreground.Yellow, GrateConsoleColor.Background.Black), - LogLevel.Error => new ConsoleColors(GrateConsoleColor.Foreground.Black, GrateConsoleColor.Background.DarkRed), - LogLevel.Critical => new ConsoleColors(GrateConsoleColor.Foreground.White, GrateConsoleColor.Background.DarkRed), + LogLevel.Trace => new ConsoleColors(GrateConsoleColor.Foreground.DarkYellow), + LogLevel.Debug => new ConsoleColors(GrateConsoleColor.Foreground.DarkGray), + LogLevel.Information => new ConsoleColors(GrateConsoleColor.Foreground.Green), + LogLevel.Warning => new ConsoleColors(GrateConsoleColor.Foreground.Yellow), + LogLevel.Error => new ConsoleColors(GrateConsoleColor.Foreground.Black), + LogLevel.Critical => new ConsoleColors(GrateConsoleColor.Foreground.White), _ => ConsoleColors.None }; } @@ -87,15 +87,13 @@ private static ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel) private readonly struct ConsoleColors { - public ConsoleColors(GrateConsoleColor foreground, GrateConsoleColor background) + public ConsoleColors(GrateConsoleColor foreground) { Foreground = foreground; - Background = background; } public GrateConsoleColor Foreground { get; } - public GrateConsoleColor Background { get; } public static ConsoleColors None => new(); } -} \ No newline at end of file +} diff --git a/grate/Infrastructure/OracleSyntax.cs b/grate/Infrastructure/OracleSyntax.cs index 9c9f135e..dbc1a99b 100644 --- a/grate/Infrastructure/OracleSyntax.cs +++ b/grate/Infrastructure/OracleSyntax.cs @@ -11,7 +11,7 @@ public string StatementSeparatorRegex const string strings = @"(?'[^']*')"; const string dashComments = @"(?--.*$)"; const string starComments = @"(?/\*[\S\s]*?\*/)"; - const string separator = @"(?^|\s)(?GO)(?\s|;|$)"; + const string separator = @"(?^|\s)(?/)(?\s|;|$)"; return strings + "|" + dashComments + "|" + starComments + "|" + separator; } } @@ -54,4 +54,4 @@ FOR ln_cur IN (SELECT sid, serial# FROM v$session WHERE username = usr) public string Quote(string text) => $"\"{text}\""; public string PrimaryKeyConstraint(string tableName, string column) => ""; public string LimitN(string sql, int n) => sql + $"\nLIMIT {n}"; -} \ No newline at end of file +} diff --git a/grate/Infrastructure/PostgreSqlSyntax.cs b/grate/Infrastructure/PostgreSqlSyntax.cs index 06b88189..06673024 100644 --- a/grate/Infrastructure/PostgreSqlSyntax.cs +++ b/grate/Infrastructure/PostgreSqlSyntax.cs @@ -24,6 +24,7 @@ public string StatementSeparatorRegex public string CreateSchema(string schemaName) => @$"CREATE SCHEMA ""{schemaName}"";"; public string CreateDatabase(string databaseName, string? _) => @$"CREATE DATABASE ""{databaseName}"""; public string DropDatabase(string databaseName) => @$"select pg_terminate_backend(pid) from pg_stat_activity where datname='{databaseName}'; + COMMIT; DROP DATABASE IF EXISTS ""{databaseName}"";"; public string TableWithSchema(string schemaName, string tableName) => $"{schemaName}.\"{tableName}\""; public string ReturnId => "RETURNING id;"; @@ -31,4 +32,4 @@ public string StatementSeparatorRegex public string Quote(string text) => $"\"{text}\""; public string PrimaryKeyConstraint(string tableName, string column) => $",\nCONSTRAINT PK_{tableName}_{column} PRIMARY KEY ({column})"; public string LimitN(string sql, int n) => sql + $"\nLIMIT {n}"; -} \ No newline at end of file +} diff --git a/grate/Infrastructure/TextWriterExtensions.cs b/grate/Infrastructure/TextWriterExtensions.cs index 1d6aeaf7..41198625 100644 --- a/grate/Infrastructure/TextWriterExtensions.cs +++ b/grate/Infrastructure/TextWriterExtensions.cs @@ -7,30 +7,30 @@ internal static class TextWriterExtensions { private static bool? _supportsAnsiColors; - public static void WriteColoredMessage(this TextWriter textWriter, string message, GrateConsoleColor background, GrateConsoleColor foreground) + public static void WriteColoredMessage(this TextWriter textWriter, string message, GrateConsoleColor foreground) { - SetColorsIfEnabled(textWriter, background, foreground); + SetColorsIfEnabled(textWriter, foreground); textWriter.Write(message); ResetColorsIfEnabled(textWriter); } - - public static void WriteColoredMessageLine(this TextWriter textWriter, string? message, GrateConsoleColor background, GrateConsoleColor foreground) + + public static void WriteColoredMessageLine(this TextWriter textWriter, string? message, GrateConsoleColor foreground) { - SetColorsIfEnabled(textWriter, background, foreground); + SetColorsIfEnabled(textWriter, foreground); textWriter.WriteLine(message); ResetColorsIfEnabled(textWriter); } - - - private static void SetColorsIfEnabled(TextWriter textWriter, GrateConsoleColor background, GrateConsoleColor foreground) + + + private static void SetColorsIfEnabled(TextWriter textWriter, GrateConsoleColor foreground) { if (!DisableAnsiColors) { - SetColors(textWriter, background.AnsiColorCode, foreground.AnsiColorCode); + SetColors(textWriter, foreground.AnsiColorCode); } } - + private static void ResetColorsIfEnabled(TextWriter textWriter) { if (!DisableAnsiColors) @@ -38,32 +38,30 @@ private static void ResetColorsIfEnabled(TextWriter textWriter) ResetColors(textWriter); } } - + private static void ResetColors(TextWriter textWriter) { textWriter.Write(GrateConsoleColor.Foreground.Default.AnsiColorCode); // reset to default foreground color - textWriter.Write(GrateConsoleColor.Background.Default.AnsiColorCode); // reset to the background color } - private static void SetColors(TextWriter textWriter, string backgroundColorAnsiCode, string foregroundColorAnsiCode) + private static void SetColors(TextWriter textWriter, string foregroundColorAnsiCode) { - textWriter.Write(backgroundColorAnsiCode); textWriter.Write(foregroundColorAnsiCode); } private static bool DisableAnsiColors => !SupportsAnsiColors || Console.IsOutputRedirected; - + private static bool SupportsAnsiColors => _supportsAnsiColors ??= GetSupportsAnsiColors(); private static bool GetSupportsAnsiColors() { try - // Calling Console.GetCursorPosition() sometimes fails if the console has not been written to yet + // Calling Console.GetCursorPosition() sometimes fails if the console has not been written to yet { lock (Console.Out) { var (oldPosition, _) = Console.GetCursorPosition(); - SetColors(Console.Out, GrateConsoleColor.Background.Gray.AnsiColorCode, GrateConsoleColor.Foreground.Blue.AnsiColorCode); + SetColors(Console.Out, GrateConsoleColor.Foreground.Blue.AnsiColorCode); var (currentPosition, yPos) = Console.GetCursorPosition(); ResetColors(Console.Out); @@ -84,4 +82,4 @@ private static bool GetSupportsAnsiColors() return true; } } -} \ No newline at end of file +} diff --git a/grate/Migration/AnsiSqlDatabase.cs b/grate/Migration/AnsiSqlDatabase.cs index 74b68b2c..3930f827 100644 --- a/grate/Migration/AnsiSqlDatabase.cs +++ b/grate/Migration/AnsiSqlDatabase.cs @@ -18,6 +18,9 @@ namespace grate.Migration; public abstract class AnsiSqlDatabase : IDatabase { + private const string Now = "now"; + private const string User = "usr"; + private string SchemaName { get; set; } = ""; protected GrateConfiguration? Config { get; private set; } @@ -43,14 +46,18 @@ protected AnsiSqlDatabase(ILogger logger, ISyntax syntax) .Split("=", TrimEntries | RemoveEmptyEntries).Last(); public abstract bool SupportsDdlTransactions { get; } - protected abstract bool SupportsSchemas { get; } + public abstract bool SupportsSchemas { get; } public bool SplitBatchStatements => true; public string StatementSeparatorRegex => _syntax.StatementSeparatorRegex; - public string ScriptsRunTable => _syntax.TableWithSchema(SchemaName, "ScriptsRun"); - public string ScriptsRunErrorsTable => _syntax.TableWithSchema(SchemaName, "ScriptsRunErrors"); - public string VersionTable => _syntax.TableWithSchema(SchemaName, "Version"); + public string ScriptsRunTable => _syntax.TableWithSchema(SchemaName, ScriptsRunTableName); + public string ScriptsRunErrorsTable => _syntax.TableWithSchema(SchemaName, ScriptsRunErrorsTableName); + public string VersionTable => _syntax.TableWithSchema(SchemaName, VersionTableName); + + private string ScriptsRunTableName { get; set; } + private string ScriptsRunErrorsTableName { get; set; } + private string VersionTableName { get; set; } public virtual Task InitializeConnections(GrateConfiguration configuration) { @@ -58,11 +65,23 @@ public virtual Task InitializeConnections(GrateConfiguration configuration) ConnectionString = configuration.ConnectionString; AdminConnectionString = configuration.AdminConnectionString; + SchemaName = configuration.SchemaName; + + VersionTableName = configuration.VersionTableName; + ScriptsRunTableName = configuration.ScriptsRunTableName; + ScriptsRunErrorsTableName = configuration.ScriptsRunErrorsTableName; + Config = configuration; + return Task.CompletedTask; } + private async Task ExistingOrDefault(string schemaName, string tableName) => + await ExistingTable(schemaName, tableName) ?? tableName; + + + private string? AdminConnectionString { get; set; } protected string? ConnectionString { get; set; } @@ -254,7 +273,7 @@ private async Task CreateRunSchema() private async Task RunSchemaExists() { - string sql = $"SELECT s.schema_name FROM information_schema.schemata s WHERE s.schema_name = '{SchemaName}'"; + string sql = $"SELECT s.SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA s WHERE s.SCHEMA_NAME = '{SchemaName}'"; var res = await ExecuteScalarAsync(ActiveConnection, sql); return res != null; // #230: If the server found a record that's good enough for us } @@ -263,6 +282,10 @@ private async Task RunSchemaExists() protected virtual async Task CreateScriptsRunTable() { + // Update scripts run table name with the correct casing, should it differ from the standard + + ScriptsRunTableName = await ExistingOrDefault(SchemaName, ScriptsRunTableName); + string createSql = $@" CREATE TABLE {ScriptsRunTable}( {_syntax.PrimaryKeyColumn("id")}, @@ -285,6 +308,9 @@ protected virtual async Task CreateScriptsRunTable() protected virtual async Task CreateScriptsRunErrorsTable() { + // Update scripts run errors table name with the correct casing, should it differ from the standard + ScriptsRunErrorsTableName = await ExistingOrDefault(SchemaName, ScriptsRunErrorsTableName); + string createSql = $@" CREATE TABLE {ScriptsRunErrorsTable}( {_syntax.PrimaryKeyColumn("id")}, @@ -307,6 +333,9 @@ protected virtual async Task CreateScriptsRunErrorsTable() protected virtual async Task CreateVersionTable() { + // Update version table name with the correct casing, should it differ from the standard + VersionTableName = await ExistingOrDefault(SchemaName, VersionTableName); + string createSql = $@" CREATE TABLE {VersionTable}( {_syntax.PrimaryKeyColumn("id")}, @@ -317,6 +346,7 @@ protected virtual async Task CreateVersionTable() entered_by {_syntax.VarcharType}(50) NULL {_syntax.PrimaryKeyConstraint("Version", "id")} )"; + if (!await VersionTableExists()) { await ExecuteNonQuery(ActiveConnection, createSql, Config?.CommandTimeout); @@ -335,21 +365,25 @@ ALTER TABLE {VersionTable} } } - protected async Task ScriptsRunTableExists() => await TableExists(SchemaName, "ScriptsRun"); - protected async Task ScriptsRunErrorsTableExists() => await TableExists(SchemaName, "ScriptsRunErrors"); - public async Task VersionTableExists() => await TableExists(SchemaName, "Version"); - protected async Task StatusColumnInVersionTableExists() => await ColumnExists(SchemaName, "Version", "status"); + protected async Task ScriptsRunTableExists() => (await ExistingTable(SchemaName, ScriptsRunTableName) is not null) ; + protected async Task ScriptsRunErrorsTableExists() => (await ExistingTable(SchemaName, ScriptsRunErrorsTableName) is not null); + public async Task VersionTableExists() => (await ExistingTable(SchemaName, VersionTableName) is not null); + + protected async Task StatusColumnInVersionTableExists() => await ColumnExists(SchemaName, VersionTableName, "status"); - public async Task TableExists(string schemaName, string tableName) + public async Task ExistingTable(string schemaName, string tableName) { var fullTableName = SupportsSchemas ? tableName : _syntax.TableWithSchema(schemaName, tableName); var tableSchema = SupportsSchemas ? schemaName : DatabaseName; - + string existsSql = ExistsSql(tableSchema, fullTableName); var res = await ExecuteScalarAsync(ActiveConnection, existsSql); - return !DBNull.Value.Equals(res) && res is not null; + var name = (!DBNull.Value.Equals(res) && res is not null) ? (string) res : null; + + var prefix = SupportsSchemas ? string.Empty : _syntax.TableWithSchema(schemaName, string.Empty); + return name?[prefix.Length..] ; } private async Task ColumnExists(string schemaName, string tableName, string columnName) @@ -366,21 +400,21 @@ private async Task ColumnExists(string schemaName, string tableName, strin protected virtual string ExistsSql(string tableSchema, string fullTableName) { return $@" -SELECT * FROM information_schema.tables +SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE -table_schema = '{tableSchema}' AND -table_name = '{fullTableName}' +LOWER(TABLE_SCHEMA) = LOWER('{tableSchema}') AND +LOWER(TABLE_NAME) = LOWER('{fullTableName}') "; } protected virtual string ExistsSql(string tableSchema, string fullTableName, string columnName) { return $@" -SELECT * FROM information_schema.columns +SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE -table_schema = '{tableSchema}' AND -table_name = '{fullTableName}' AND -column_name = '{columnName}' +LOWER(TABLE_SCHEMA) = LOWER('{tableSchema}') AND +LOWER(TABLE_NAME) = LOWER('{fullTableName}') AND +LOWER(COLUMN_NAME) = LOWER('{columnName}') "; } @@ -576,16 +610,14 @@ INSERT INTO {ScriptsRunTable} (version_id, script_name, text_of_script, text_hash, one_time_script, entry_date, modified_date, entered_by) VALUES (@versionId, @scriptName, @sql, @hash, @runOnce, @now, @now, @usr)"); - var scriptRun = new - { - versionId, - scriptName, - sql, - hash, - runOnce = Bool(runOnce), - now = DateTime.UtcNow, - usr = Environment.UserName - }; + var scriptRun = new DynamicParameters(); + scriptRun.Add(nameof(versionId), versionId); + scriptRun.Add(nameof(scriptName), scriptName); + scriptRun.Add(nameof(sql), sql, DbType.String); + scriptRun.Add(nameof(hash), hash); + scriptRun.Add(nameof(runOnce), Bool(runOnce)); + scriptRun.Add(Now, DateTime.UtcNow); + scriptRun.Add(User, Environment.UserName); await ExecuteAsync(ActiveConnection, insertSql, scriptRun); } @@ -601,16 +633,14 @@ INSERT INTO {ScriptsRunErrorsTable} var version = await ExecuteScalarAsync(ActiveConnection, versionSql, new { versionId }); - var scriptRunErrors = new - { - version, - scriptName, - sql, - errorSql, - errorMessage, - now = DateTime.UtcNow, - usr = Environment.UserName, - }; + var scriptRunErrors = new DynamicParameters(); + scriptRunErrors.Add(nameof(version), version); + scriptRunErrors.Add(nameof(scriptName), scriptName); + scriptRunErrors.Add(nameof(sql), sql, DbType.String); + scriptRunErrors.Add(nameof(errorSql), errorSql, DbType.String); + scriptRunErrors.Add(nameof(errorMessage), errorMessage, DbType.String); + scriptRunErrors.Add(Now, DateTime.UtcNow); + scriptRunErrors.Add(User, Environment.UserName); await ExecuteAsync(ActiveConnection, insertSql, scriptRunErrors); } diff --git a/grate/Migration/DbMigrator.cs b/grate/Migration/DbMigrator.cs index a48df486..aa343efe 100644 --- a/grate/Migration/DbMigrator.cs +++ b/grate/Migration/DbMigrator.cs @@ -154,6 +154,42 @@ async Task LogAndRunSql() return theSqlWasRun; } + + public async Task RunSqlWithoutLogging( + string sql, + string scriptName, + GrateEnvironment? environment, + ConnectionType connectionType, + TransactionHandling transactionHandling) + { + async Task PrintLogAndRunSql() + { + _logger.LogInformation(" Running '{ScriptName}'.", scriptName); + + if (Configuration.DryRun) + { + return false; + } + else + { + await RunTheActualSqlWithoutLogging(sql, scriptName, connectionType, transactionHandling); + return true; + } + } + + if (!InCorrectEnvironment(scriptName, environment)) + { + return false; + } + + if (TokenReplacementEnabled) + { + sql = ReplaceTokensIn(sql); + } + + return await PrintLogAndRunSql();; + } + public async Task RestoreDatabase(string backupPath) { @@ -273,6 +309,34 @@ private async Task RunTheActualSql(string sql, await RecordScriptInScriptsRunTable(scriptName, sql, migrationType, versionId, transactionHandling); } + + private async Task RunTheActualSqlWithoutLogging( + string sql, + string scriptName, + ConnectionType connectionType, + TransactionHandling transactionHandling) + { + foreach (var statement in GetStatements(sql)) + { + try + { + await Database.RunSql(statement, connectionType, transactionHandling); + } + catch (Exception ex) + { + _logger.LogError("Error running script \"{ScriptName}\": {ErrorMessage}", scriptName, ex.Message); + + if (Transaction.Current is not null) { + Database.Rollback(); + } + + await Database.CloseConnection(); + Transaction.Current?.Dispose(); + throw; + } + } + } + private IEnumerable GetStatements(string sql) => StatementSplitter.Split(sql); diff --git a/grate/Migration/FileSystem.cs b/grate/Migration/FileSystem.cs index eb16d9c5..0402608e 100644 --- a/grate/Migration/FileSystem.cs +++ b/grate/Migration/FileSystem.cs @@ -1,16 +1,26 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; +using static System.IO.Path; +using static System.IO.SearchOption; +using static System.StringComparer; namespace grate.Migration; public static class FileSystem { - public static IEnumerable GetFiles(DirectoryInfo folderPath, string pattern) + public static IEnumerable GetFiles(DirectoryInfo folderPath, string pattern, bool ignoreDirectoryNames = false) { - return folderPath - .EnumerateFileSystemInfos(pattern, SearchOption.AllDirectories).ToList() - .OrderBy(f => Path.GetRelativePath(folderPath.ToString(), f.FullName), StringComparer.CurrentCultureIgnoreCase); + return ignoreDirectoryNames + ? folderPath + .EnumerateFileSystemInfos(pattern, AllDirectories).ToList() + .OrderBy(f => GetFileNameWithoutExtension(f.FullName), CurrentCultureIgnoreCase) + : folderPath + .EnumerateFileSystemInfos(pattern, AllDirectories).ToList() + .OrderBy(f => + Combine( + GetRelativePath(folderPath.ToString(), GetDirectoryName(f.FullName)!), + GetFileNameWithoutExtension(f.FullName)), + CurrentCultureIgnoreCase); } } diff --git a/grate/Migration/GrateMigrator.cs b/grate/Migration/GrateMigrator.cs index ae4be1fc..90c61474 100644 --- a/grate/Migration/GrateMigrator.cs +++ b/grate/Migration/GrateMigrator.cs @@ -229,7 +229,7 @@ private async Task CreateGrateStructure(IDbMigrator dbMigrator) return (versionId, newVersion); } - private static async Task CreateDatabaseIfItDoesNotExist(IDbMigrator dbMigrator) + private async Task CreateDatabaseIfItDoesNotExist(IDbMigrator dbMigrator) { bool databaseCreated; if (await dbMigrator.DatabaseExists()) @@ -238,7 +238,28 @@ private static async Task CreateDatabaseIfItDoesNotExist(IDbMigrator dbMig } else { - databaseCreated = await dbMigrator.CreateDatabase(); + var config = dbMigrator.Configuration; + var createDatabaseFolder = config.Folders?.CreateDatabase; + var database = _migrator.Database; + + var path = Wrap(config.SqlFilesDirectory, createDatabaseFolder?.Path ?? "zz-xx-øø-definitely-does-not-exist"); + + if (createDatabaseFolder is not null && path.Exists) + { + //await LogAndProcess(config.SqlFilesDirectory, folder!, changeDropFolder, versionId, folder!.ConnectionType, folder.TransactionHandling); + var changeDropFolder = ChangeDropFolder(config, database.ServerName, database.DatabaseName); + databaseCreated = await ProcessWithoutLogging( + config.SqlFilesDirectory, + createDatabaseFolder, + changeDropFolder, + createDatabaseFolder.ConnectionType, + createDatabaseFolder.TransactionHandling + ); + } + else + { + databaseCreated = await dbMigrator.CreateDatabase(); + } } return databaseCreated; } @@ -295,7 +316,7 @@ private async Task Process(DirectoryInfo root, MigrationsFolder folder, string c await EnsureConnectionIsOpen(connectionType); var pattern = "*.sql"; - var files = FileSystem.GetFiles(path, pattern); + var files = FileSystem.GetFiles(path, pattern, _migrator.Configuration.IgnoreDirectoryNames); var anySqlRun = false; @@ -304,8 +325,9 @@ private async Task Process(DirectoryInfo root, MigrationsFolder folder, string c var sql = await File.ReadAllTextAsync(file.FullName); // Normalize file names to log, so that results won't vary if you run on *nix VS Windows - var fileNameToLog = string.Join('/', - Path.GetRelativePath(path.ToString(), file.FullName).Split(Path.DirectorySeparatorChar)); + var fileNameToLog = _migrator.Configuration.IgnoreDirectoryNames + ? file.Name + : string.Join('/', Path.GetRelativePath(path.ToString(), file.FullName).Split(Path.DirectorySeparatorChar)); bool theSqlRan = await _migrator.RunSql(sql, fileNameToLog, folder.Type, versionId, _migrator.Configuration.Environment, connectionType, transactionHandling); @@ -324,13 +346,61 @@ private async Task Process(DirectoryInfo root, MigrationsFolder folder, string c } } + if (!anySqlRun && !_migrator.Configuration.DryRun) + { + _logger.LogInformation(" No sql run, either an empty folder, or all files run against destination previously."); + } + + } + + private async Task ProcessWithoutLogging(DirectoryInfo root, MigrationsFolder folder, string changeDropFolder, + ConnectionType connectionType, TransactionHandling transactionHandling) + { + var path = Wrap(root, folder.Path); + + await EnsureConnectionIsOpen(connectionType); + + var pattern = "*.sql"; + var files = FileSystem.GetFiles(path, pattern, _migrator.Configuration.IgnoreDirectoryNames); + + var anySqlRun = false; + + foreach (var file in files) + { + var sql = await File.ReadAllTextAsync(file.FullName); + + // Normalize file names to log, so that results won't vary if you run on *nix VS Windows + var fileNameToLog = _migrator.Configuration.IgnoreDirectoryNames + ? file.Name + : string.Join('/', Path.GetRelativePath(path.ToString(), file.FullName).Split(Path.DirectorySeparatorChar)); + + bool theSqlRan = await _migrator.RunSqlWithoutLogging(sql, fileNameToLog, _migrator.Configuration.Environment, + connectionType, transactionHandling); + + if (theSqlRan) + { + anySqlRun = true; + try + { + CopyToChangeDropFolder(path.Parent!, file, changeDropFolder); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Unable to copy {File} to {ChangeDropFolder}. \n{Exception}", file, changeDropFolder, ex.Message); + } + } + } + if (!anySqlRun) { _logger.LogInformation(" No sql run, either an empty folder, or all files run against destination previously."); } + return anySqlRun; + } + private void CopyToChangeDropFolder(DirectoryInfo migrationRoot, FileSystemInfo file, string changeDropFolder) { var relativePath = Path.GetRelativePath(migrationRoot.ToString(), file.FullName); diff --git a/grate/Migration/IDatabase.cs b/grate/Migration/IDatabase.cs index 708b13d7..26755cef 100644 --- a/grate/Migration/IDatabase.cs +++ b/grate/Migration/IDatabase.cs @@ -17,6 +17,7 @@ public interface IDatabase : IAsyncDisposable public string ScriptsRunErrorsTable { get; } public string VersionTable { get; } DbConnection ActiveConnection { set; } + bool SupportsSchemas { get; } Task InitializeConnections(GrateConfiguration configuration); Task OpenConnection(); @@ -48,4 +49,5 @@ Task InsertScriptRun(string scriptName, string? sql, string hash, bool runOnce, void SetDefaultConnectionActive(); Task OpenNewActiveConnection(); Task OpenActiveConnection(); + Task ExistingTable(string schemaName, string tableName); } diff --git a/grate/Migration/IDbMigrator.cs b/grate/Migration/IDbMigrator.cs index 2ee074a2..6cb60fac 100644 --- a/grate/Migration/IDbMigrator.cs +++ b/grate/Migration/IDbMigrator.cs @@ -26,10 +26,15 @@ public interface IDbMigrator: IAsyncDisposable Task VersionTheDatabase(string newVersion); Task OpenAdminConnection(); Task CloseAdminConnection(); + Task RunSql(string sql, string scriptName, MigrationType migrationType, long versionId, GrateEnvironment? environment, ConnectionType connectionType, TransactionHandling transactionHandling); + Task RunSqlWithoutLogging(string sql, string scriptName, + GrateEnvironment? environment, + ConnectionType connectionType, TransactionHandling transactionHandling); + Task RestoreDatabase(string backupPath); void SetDefaultConnectionActive(); Task OpenNewActiveConnection(); diff --git a/grate/Migration/MariaDbDatabase.cs b/grate/Migration/MariaDbDatabase.cs index 9091573a..93cda516 100644 --- a/grate/Migration/MariaDbDatabase.cs +++ b/grate/Migration/MariaDbDatabase.cs @@ -15,7 +15,7 @@ public MariaDbDatabase(ILogger logger) { } public override bool SupportsDdlTransactions => false; - protected override bool SupportsSchemas => false; + public override bool SupportsSchemas => false; protected override DbConnection GetSqlConnection(string? connectionString) => new MySqlConnection(connectionString); public override Task RestoreDatabase(string backupPath) @@ -30,7 +30,7 @@ public override async Task DropDatabase() // We need to kill any active connections to get MariaDB to actually delete the database, // and stop accepting new connections to it. So we create a list of the - // active sessions against our databse, and create 'KILL X' statements (where X is session id). + // active sessions against our database, and create 'KILL X' statements (where X is session id). // Then we execute the kill statements. var sql = $@" SELECT GROUP_CONCAT(CONCAT('KILL ',id,';') SEPARATOR ' ') @@ -39,7 +39,7 @@ public override async Task DropDatabase() var killStatements = await ExecuteScalarAsync(AdminConnection, sql); if (killStatements != null && !DBNull.Value.Equals(killStatements)) { - string killSql = killStatements.ToString() ?? ""; // Just to keel warnings happy + string killSql = killStatements.ToString() ?? ""; // Just to keep warnings happy await ExecuteNonQuery(AdminConnection, killSql, null); } diff --git a/grate/Migration/OracleDatabase.cs b/grate/Migration/OracleDatabase.cs index f6f7080c..576a1d1d 100644 --- a/grate/Migration/OracleDatabase.cs +++ b/grate/Migration/OracleDatabase.cs @@ -23,13 +23,13 @@ public OracleDatabase(ILogger logger) } public override bool SupportsDdlTransactions => false; - protected override bool SupportsSchemas => false; + public override bool SupportsSchemas => false; protected override DbConnection GetSqlConnection(string? connectionString) => new OracleConnection(connectionString); protected override string ExistsSql(string tableSchema, string fullTableName) => $@" -SELECT * FROM user_tables +SELECT table_name FROM user_tables WHERE lower(table_name) = '{fullTableName.ToLowerInvariant()}' "; @@ -73,7 +73,7 @@ protected override async Task CreateScriptsRunErrorsTable() public override Task RestoreDatabase(string backupPath) { - throw new System.NotImplementedException("Restoring a database from file is not currently supported for Maria DB."); + throw new System.NotImplementedException("Restoring a database from file is not currently supported for Oracle."); } protected override async Task CreateVersionTable() diff --git a/grate/Migration/PostgreSqlDatabase.cs b/grate/Migration/PostgreSqlDatabase.cs index 5ac994e8..a402d1dd 100644 --- a/grate/Migration/PostgreSqlDatabase.cs +++ b/grate/Migration/PostgreSqlDatabase.cs @@ -13,7 +13,7 @@ public PostgreSqlDatabase(ILogger logger) { } public override bool SupportsDdlTransactions => true; - protected override bool SupportsSchemas => true; + public override bool SupportsSchemas => true; protected override DbConnection GetSqlConnection(string? connectionString) => new NpgsqlConnection(connectionString); public override Task RestoreDatabase(string backupPath) diff --git a/grate/Migration/SqLiteDatabase.cs b/grate/Migration/SqLiteDatabase.cs index 166324a2..a04da752 100644 --- a/grate/Migration/SqLiteDatabase.cs +++ b/grate/Migration/SqLiteDatabase.cs @@ -17,16 +17,15 @@ public SqliteDatabase(ILogger logger) { } public override bool SupportsDdlTransactions => false; - protected override bool SupportsSchemas => false; + public override bool SupportsSchemas => false; protected override DbConnection GetSqlConnection(string? connectionString) => new SqliteConnection(connectionString); protected override string ExistsSql(string tableSchema, string fullTableName) => $@" SELECT name FROM sqlite_master WHERE type ='table' AND -name = '{fullTableName}' COLLATE NOCASE; -"; // #230: Correct mismatched schema casing, sqllite is case-insensitive but the string comparisons in queries _are_ case sensitive by default - +LOWER(name) = LOWER('{fullTableName}'); +"; protected override string ExistsSql(string tableSchema, string fullTableName, string columnName) => $@"SELECT * FROM pragma_table_info('{fullTableName}') diff --git a/grate/Migration/SqlServerDatabase.cs b/grate/Migration/SqlServerDatabase.cs index 68e33a44..bf98f57c 100644 --- a/grate/Migration/SqlServerDatabase.cs +++ b/grate/Migration/SqlServerDatabase.cs @@ -16,9 +16,18 @@ public SqlServerDatabase(ILogger logger) { } public override bool SupportsDdlTransactions => true; - protected override bool SupportsSchemas => true; + public override bool SupportsSchemas => true; protected override DbConnection GetSqlConnection(string? connectionString) { + // If pooling is not explicitly mentioned in the connection string, turn it off, as enabling it + // might lead to problems in more scenarios than it (potentially) solves, in the most + // common grate scenarios. + if (!(connectionString ?? "").Contains("Pooling", StringComparison.InvariantCultureIgnoreCase)) + { + var builder = new SqlConnectionStringBuilder(connectionString) { Pooling = false }; + connectionString = builder.ConnectionString; + } + var conn = new SqlConnection(connectionString); conn.AccessToken = AccessToken; diff --git a/grate/grate.csproj b/grate/grate.csproj index 00d861b7..1587f984 100644 --- a/grate/grate.csproj +++ b/grate/grate.csproj @@ -3,6 +3,7 @@ Exe net6.0 + net6.0;net7.0 Embedded enable MIT @@ -12,7 +13,7 @@ grate - sql for the 20s grate is a no-code, low-fi database migration tool, inspired heavily by RoundhousE. It's written from the ground -up using modern .NET 6. +up using modern .NET 6/7. https://erikbra.github.io/grate/ https://github.com/erikbra/grate @@ -21,15 +22,15 @@ up using modern .NET 6. - - - + + + - + - - - + + + @@ -55,4 +56,8 @@ up using modern .NET 6. true + + false + +