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