diff --git a/ClickView.GoodStuff.sln b/ClickView.GoodStuff.sln index dfe85dc0..ef426729 100644 --- a/ClickView.GoodStuff.sln +++ b/ClickView.GoodStuff.sln @@ -74,6 +74,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{63D140 .github\workflows\release.yaml = .github\workflows\release.yaml EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Snowflake", "Snowflake", "{4B8ED14E-7CFF-4123-B54D-3BEBAB817262}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClickView.GoodStuff.Repositories.Snowflake", "src\Repositories\Snowflake\src\ClickView.GoodStuff.Repositories.Snowflake.csproj", "{E55CD02D-A938-4FD0-8BB2-D77943976DD0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClickView.GoodStuff.Repositories.Snowflake.Tests", "src\Repositories\Snowflake\test\ClickView.GoodStuff.Repositories.Snowflake.Tests.csproj", "{609D602D-1DBA-4A16-B17A-1AB4CF8E8E94}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -156,6 +162,14 @@ Global {292AD84D-B42C-4DB6-882A-509860B4754F}.Debug|Any CPU.Build.0 = Debug|Any CPU {292AD84D-B42C-4DB6-882A-509860B4754F}.Release|Any CPU.ActiveCfg = Release|Any CPU {292AD84D-B42C-4DB6-882A-509860B4754F}.Release|Any CPU.Build.0 = Release|Any CPU + {E55CD02D-A938-4FD0-8BB2-D77943976DD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E55CD02D-A938-4FD0-8BB2-D77943976DD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E55CD02D-A938-4FD0-8BB2-D77943976DD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E55CD02D-A938-4FD0-8BB2-D77943976DD0}.Release|Any CPU.Build.0 = Release|Any CPU + {609D602D-1DBA-4A16-B17A-1AB4CF8E8E94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {609D602D-1DBA-4A16-B17A-1AB4CF8E8E94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {609D602D-1DBA-4A16-B17A-1AB4CF8E8E94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {609D602D-1DBA-4A16-B17A-1AB4CF8E8E94}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -188,6 +202,9 @@ Global {FED685BF-255A-4ED1-80ED-B7596ABC337B} = {0578918C-A83D-4FB0-BFD8-50639DB46AF0} {292AD84D-B42C-4DB6-882A-509860B4754F} = {046FE4C7-4E48-4CD5-BF39-3AF8110DA1C9} {63D140DE-F2A6-4644-9A1A-ABA3F2314C0A} = {A9533DB9-A1E9-4376-806E-926176FD9B13} + {4B8ED14E-7CFF-4123-B54D-3BEBAB817262} = {17A5F05F-B3EB-4011-A58C-BB1BB7995692} + {E55CD02D-A938-4FD0-8BB2-D77943976DD0} = {4B8ED14E-7CFF-4123-B54D-3BEBAB817262} + {609D602D-1DBA-4A16-B17A-1AB4CF8E8E94} = {4B8ED14E-7CFF-4123-B54D-3BEBAB817262} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F935033F-FAFD-48D4-843C-C7C8A9AE6562} diff --git a/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs b/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs new file mode 100644 index 00000000..63109af2 --- /dev/null +++ b/src/Repositories/Snowflake/src/BaseSnowflakeRepository.cs @@ -0,0 +1,123 @@ +namespace ClickView.GoodStuff.Repositories.Snowflake +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using Abstractions; + using Dapper; + using global::Snowflake.Data.Client; + + public class BaseSnowflakeRepository : BaseRepository + { + public BaseSnowflakeRepository(ISnowflakeConnectionFactory connectionFactory) : base( + connectionFactory) + { + } + + /// + /// Executes a write command + /// + /// + /// + /// + protected async Task ExecuteAsync(string sql, object? param = null) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await using var conn = GetWriteConnection(); +#else + using var conn = GetWriteConnection(); +#endif + + return await conn.ExecuteAsync(sql, param); + } + + /// + /// Executes a write command which selects a single value + /// + /// + /// + /// + /// + protected async Task ExecuteScalarAsync(string sql, object? param = null) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await using var conn = GetWriteConnection(); +#else + using var conn = GetWriteConnection(); +#endif + + return await conn.ExecuteScalarAsync(sql, param); + } + + /// + /// Executes a single value query + /// + /// + /// + /// + /// + protected async Task QueryScalarValueAsync(string sql, object? param = null) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await using var conn = GetReadConnection(); +#else + using var conn = GetReadConnection(); +#endif + + return await conn.ExecuteScalarAsync(sql, param); + } + + /// + /// Executes a single row query + /// + /// + /// + /// + /// + protected async Task QueryFirstOrDefaultAsync(string sql, object? param = null) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await using var conn = GetReadConnection(); +#else + using var conn = GetReadConnection(); +#endif + + return await conn.QueryFirstOrDefaultAsync(sql, param); + } + + /// + /// Executes a single row query and throws an exception if more than one record is found + /// + /// + /// + /// + /// + protected async Task QuerySingleOrDefaultAsync(string sql, object? param = null) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await using var conn = GetReadConnection(); +#else + using var conn = GetReadConnection(); +#endif + + return await conn.QuerySingleOrDefaultAsync(sql, param); + } + + /// + /// Executes a multiple row query + /// + /// + /// + /// + /// + protected async Task> QueryAsync(string sql, object? param = null) + { +#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER + await using var conn = GetReadConnection(); +#else + using var conn = GetReadConnection(); +#endif + + return await conn.QueryAsync(sql, param); + } + } +} \ No newline at end of file diff --git a/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj b/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj new file mode 100644 index 00000000..457df372 --- /dev/null +++ b/src/Repositories/Snowflake/src/ClickView.GoodStuff.Repositories.Snowflake.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0;net6.0 + true + + + + + + + + + + + + diff --git a/src/Repositories/Snowflake/src/ISnowflakeConnectionFactory.cs b/src/Repositories/Snowflake/src/ISnowflakeConnectionFactory.cs new file mode 100644 index 00000000..53f6738a --- /dev/null +++ b/src/Repositories/Snowflake/src/ISnowflakeConnectionFactory.cs @@ -0,0 +1,9 @@ +namespace ClickView.GoodStuff.Repositories.Snowflake +{ + using Abstractions.Factories; + using global::Snowflake.Data.Client; + + public interface ISnowflakeConnectionFactory : IConnectionFactory + { + } +} \ No newline at end of file diff --git a/src/Repositories/Snowflake/src/SnowflakeConnectionFactory.cs b/src/Repositories/Snowflake/src/SnowflakeConnectionFactory.cs new file mode 100644 index 00000000..742ba485 --- /dev/null +++ b/src/Repositories/Snowflake/src/SnowflakeConnectionFactory.cs @@ -0,0 +1,44 @@ +namespace ClickView.GoodStuff.Repositories.Snowflake +{ + using System; + using Abstractions.Factories; + using global::Snowflake.Data.Client; + + public class SnowflakeConnectionFactory : SnowflakeConnectionFactory + { + public SnowflakeConnectionFactory(ConnectionFactoryOptions options) : base(options) + { + } + } + + public class SnowflakeConnectionFactory + : ConnectionFactory, ISnowflakeConnectionFactory + where TOptions : SnowflakeConnectionOptions + { + public SnowflakeConnectionFactory(ConnectionFactoryOptions options) : base(options) + { + } + + public override SnowflakeDbConnection GetReadConnection() + { + if (string.IsNullOrEmpty(ReadConnectionString)) + throw new InvalidOperationException("Read is not allowed. No read connection options defined"); + + var connection = new SnowflakeDbConnection { ConnectionString = ReadConnectionString }; + connection.Open(); + + return connection; + } + + public override SnowflakeDbConnection GetWriteConnection() + { + if (string.IsNullOrEmpty(WriteConnectionString)) + throw new InvalidOperationException("Write is not allowed. No write connection options defined"); + + var connection = new SnowflakeDbConnection { ConnectionString = WriteConnectionString }; + connection.Open(); + + return connection; + } + } +} \ No newline at end of file diff --git a/src/Repositories/Snowflake/src/SnowflakeConnectionOptions.cs b/src/Repositories/Snowflake/src/SnowflakeConnectionOptions.cs new file mode 100644 index 00000000..a181e61b --- /dev/null +++ b/src/Repositories/Snowflake/src/SnowflakeConnectionOptions.cs @@ -0,0 +1,62 @@ +namespace ClickView.GoodStuff.Repositories.Snowflake +{ + using Abstractions; + + public class SnowflakeConnectionOptions : RepositoryConnectionOptions + { + /// + /// The full account name which might include additional segments that identify the region and + /// cloud platform where your account is hosted + /// + public string? Account + { + set => SetParameter("account", value); + get => GetParameter("account"); + } + + /// + /// The name of the warehouse to use + /// + public string? Warehouse + { + set => SetParameter("warehouse", value); + get => GetParameter("warehouse"); + } + + /// + /// The database to use + /// + public string? Database + { + set => SetParameter("db", value); + get => GetParameter("db"); + } + + /// + /// The schema to use + /// + public string? Schema + { + set => SetParameter("schema", value); + get => GetParameter("schema"); + } + + /// + /// The Snowflake user to use + /// + public string? User + { + set => SetParameter("user", value); + get => GetParameter("user"); + } + + /// + /// The password for the Snowflake user + /// + public string? Password + { + set => SetParameter("password", value); + get => GetParameter("password"); + } + } +} \ No newline at end of file diff --git a/src/Repositories/Snowflake/test/ClickView.GoodStuff.Repositories.Snowflake.Tests.csproj b/src/Repositories/Snowflake/test/ClickView.GoodStuff.Repositories.Snowflake.Tests.csproj new file mode 100644 index 00000000..34b99bcb --- /dev/null +++ b/src/Repositories/Snowflake/test/ClickView.GoodStuff.Repositories.Snowflake.Tests.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + false + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Repositories/Snowflake/test/SnowflakeConnectionOptionsTests.cs b/src/Repositories/Snowflake/test/SnowflakeConnectionOptionsTests.cs new file mode 100644 index 00000000..a5991fb1 --- /dev/null +++ b/src/Repositories/Snowflake/test/SnowflakeConnectionOptionsTests.cs @@ -0,0 +1,104 @@ +namespace ClickView.GoodStuff.Repositories.Snowflake.Tests +{ + using Xunit; + + public class SnowflakeConnectionOptionsTests + { + [Fact] + public void GetConnectionString_Default_Empty() + { + var options = new SnowflakeConnectionOptions(); + + var connString = options.GetConnectionString(); + + Assert.Equal(string.Empty, connString); + } + + [Fact] + public void GetConnectionString_AllOptionsSet_Valid() + { + var options = new SnowflakeConnectionOptions + { + Host = "host", + Account = "acc", + Warehouse = "wh", + Database = "db", + Schema = "sch", + User = "user", + Password = "pass" + }; + + var connString = options.GetConnectionString(); + + Assert.Equal( + "host=host;" + + "account=acc;" + + "warehouse=wh;" + + "db=db;" + + "schema=sch;" + + "user=user;" + + "password=pass;", + connString); + } + + [Fact] + public void PropertiesSet_AllOptionsSet_Valid() + { + var options = new SnowflakeConnectionOptions + { + Host = "host", + Account = "acc", + Warehouse = "wh", + Database = "db", + Schema = "sch", + User = "user", + Password = "pass" + }; + + Assert.Equal("host", options.Host); + Assert.Equal("acc", options.Account); + Assert.Equal("wh", options.Warehouse); + Assert.Equal("db", options.Database); + Assert.Equal("sch", options.Schema); + Assert.Equal("user", options.User); + Assert.Equal("pass", options.Password); + } + + [Fact] + public void PropertiesSet_Default_Empty() + { + var options = new SnowflakeConnectionOptions(); + + Assert.Null(options.Host); + Assert.Null(options.Account); + Assert.Null(options.Warehouse); + Assert.Null(options.Database); + Assert.Null(options.Schema); + Assert.Null(options.User); + Assert.Null(options.Password); + } + + [Fact] + public void PropertiesSet_Null_Empty() + { + var options = new SnowflakeConnectionOptions + { + Host = null, + Account = null, + Warehouse = null, + Database = null, + Schema = null, + User = null, + Password = null + }; + + Assert.Null(options.Host); + Assert.Null(options.Account); + Assert.Null(options.Warehouse); + Assert.Null(options.Database); + Assert.Null(options.Schema); + Assert.Null(options.User); + Assert.Null(options.Password); + } + } +}