diff --git a/examples/src/DapperExample/Program.cs b/examples/src/DapperExample/Program.cs index 43ab5a2b..df94e8ea 100644 --- a/examples/src/DapperExample/Program.cs +++ b/examples/src/DapperExample/Program.cs @@ -3,28 +3,25 @@ using Dapper; using Ydb.Sdk.Ado; -// TODO SQL Parser @Id -> $Id - // Init Users table -await new YdbConnection().ExecuteAsync(""" - CREATE TABLE Users( - Id Int32, - Name Text, - Email Text, - PRIMARY KEY (Id) - ); - """); - -await new YdbConnection().ExecuteAsync("INSERT INTO Users(Id, Name, Email) VALUES ($Id, $Name, $Email)", - new Dictionary { { "$Id", 1 }, { "$Name", "Name" }, { "$Email", "Email" } }); - -Console.WriteLine(await new YdbConnection().QuerySingleAsync("SELECT * FROM Users WHERE Id = $Id", - new Dictionary { { "$Id", 1 } })); - -Console.WriteLine(await new YdbConnection().QuerySingleAsync("SELECT * FROM Users WHERE Id = $Id", +await using var connection = await new YdbDataSource().OpenConnectionAsync(); + +await connection.ExecuteAsync(""" + CREATE TABLE Users( + Id Int32, + Name Text, + Email Text, + PRIMARY KEY (Id) + ); + """); + +await connection.ExecuteAsync("INSERT INTO Users(Id, Name, Email) VALUES ($Id, $Name, $Email)", + new User { Id = 1, Name = "Name", Email = "Email" }); + +Console.WriteLine(await connection.QuerySingleAsync("SELECT * FROM Users WHERE Id = $Id", new { Id = 1 })); -await new YdbConnection().ExecuteAsync("DROP TABLE Users"); +await connection.ExecuteAsync("DROP TABLE Users"); internal class User { diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index 50ba3fd1..bbdbea9b 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -168,11 +168,13 @@ protected override YdbDataReader ExecuteDbDataReader(CommandBehavior behavior) protected override async Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) { - if (YdbConnection.LastReader is { IsClosed: false }) + if (YdbConnection.IsBusy) { throw new YdbOperationInProgressException(YdbConnection); } + YdbConnection.EnsureConnectionOpen(); + var ydbParameters = DbParameterCollection.YdbParameters; var (sql, paramNames) = SqlParser.Parse(CommandText); var preparedSql = new StringBuilder(); diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index d7f0cb2f..c14187ce 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -164,7 +164,7 @@ protected override YdbCommand CreateDbCommand() return CreateDbCommand(); } - private void EnsureConnectionOpen() + internal void EnsureConnectionOpen() { if (ConnectionState == ConnectionState.Closed) { diff --git a/src/Ydb.Sdk/tests/Dapper/DapperIntegrationTests.cs b/src/Ydb.Sdk/tests/Dapper/DapperIntegrationTests.cs new file mode 100644 index 00000000..bc6c8be0 --- /dev/null +++ b/src/Ydb.Sdk/tests/Dapper/DapperIntegrationTests.cs @@ -0,0 +1,212 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Data; +using Dapper; +using Xunit; +using Ydb.Sdk.Ado; + +namespace Ydb.Sdk.Tests.Dapper; + +public class DapperIntegrationTests +{ + private static readonly TemporaryTables Tables = new(); + + [Fact] + public async Task DapperYqlTutorialTests() + { + SqlMapper.SetTypeMap( + typeof(Episode), + new CustomPropertyTypeMap( + typeof(Episode), + (type, columnName) => + type.GetProperties().FirstOrDefault(prop => + prop.GetCustomAttributes(false) + .OfType() + .Any(attr => attr.Name == columnName)) ?? throw new InvalidOperationException())); + + await using var connection = new YdbConnection(); + await connection.OpenAsync(); + + await connection.ExecuteAsync(Tables.CreateTables); // create tables + await connection.ExecuteAsync(Tables.UpsertData); // adding data to table + + var selectedEpisodes = (await connection.QueryAsync($@" +SELECT + series_id, + season_id, + episode_id, + title, + air_date + +FROM {Tables.Episodes} +WHERE + series_id = @series_id -- List of conditions to build the result + AND season_id > @season_id -- Logical AND is used for complex conditions + +ORDER BY -- Sorting the results. + series_id, -- ORDER BY sorts the values by one or multiple + season_id, -- columns. Columns are separated by commas. + episode_id + +LIMIT 3 -- LIMIT N after ORDER BY means + -- ""get top N"" or ""get bottom N"" results, +; -- depending on sort order. + ", new { series_id = 1, season_id = 1 })).ToArray(); + + Assert.Equal( + new[] + { + new Episode + { + SeriesId = 1, SeasonId = 2, EpisodeId = 1, Title = "The Work Outing", + AirDate = new DateTime(2006, 8, 24) + }, + new Episode + { + SeriesId = 1, SeasonId = 2, EpisodeId = 2, Title = "Return of the Golden Child", + AirDate = new DateTime(2007, 8, 31) + }, + new Episode + { + SeriesId = 1, SeasonId = 2, EpisodeId = 3, Title = "Moss and the German", + AirDate = new DateTime(2007, 9, 7) + } + }, selectedEpisodes); + + + var selectedTitlesSeasonAndSeries = (await connection.QueryAsync($@" +SELECT + sa.title AS season_title, -- sa and sr are ""join names"", + sr.title AS series_title, -- table aliases declared below using AS. + sr.series_id, -- They are used to avoid + sa.season_id -- ambiguity in the column names used. + +FROM + {Tables.Seasons} AS sa +INNER JOIN + {Tables.Series} AS sr +ON sa.series_id = sr.series_id +WHERE sa.series_id = @series_id +ORDER BY -- Sorting of the results. + sr.series_id, + sa.season_id -- ORDER BY sorts the values by one column +; -- or multiple columns. + -- Columns are separated by commas.", new { series_id = 1 })).ToArray(); + + for (var i = 0; i < selectedTitlesSeasonAndSeries.Length; i++) + { + Assert.Equal("IT Crowd", selectedTitlesSeasonAndSeries[i].series_title); + Assert.Equal("Season " + (i + 1), selectedTitlesSeasonAndSeries[i].season_title); + } + + var transaction = connection.BeginTransaction(); + var episode1 = new Episode + { SeriesId = 2, SeasonId = 5, EpisodeId = 13, Title = "Test Episode", AirDate = new DateTime(2018, 8, 27) }; + var episode2 = new Episode + { + SeriesId = 2, SeasonId = 5, EpisodeId = 12, Title = "Test Episode !!!", AirDate = new DateTime(2018, 8, 27) + }; + + var parameters1 = new DynamicParameters(); + parameters1.Add("series_id", episode1.SeriesId, DbType.UInt64); + parameters1.Add("season_id", episode1.SeasonId, DbType.UInt64); + parameters1.Add("episode_id", episode1.EpisodeId, DbType.UInt64); + parameters1.Add("title", episode1.Title, DbType.String); + parameters1.Add("air_date", episode1.AirDate, DbType.Date); + + await connection.ExecuteAsync($@" +UPSERT INTO {Tables.Episodes} +( + series_id, + season_id, + episode_id, + title, + air_date +) +VALUES +( + @series_id, + @season_id, + @episode_id, + @title, + @air_date +); +;", parameters1, transaction); + await using (var otherConn = new YdbConnection()) + { + await otherConn.OpenAsync(); + + Assert.Null(await otherConn.QuerySingleOrDefaultAsync( + $"SELECT * FROM {Tables.Episodes} WHERE series_id = @p1 AND season_id = @p2 AND episode_id = @p3", + new { p1 = episode1.SeriesId, p2 = episode1.SeasonId, p3 = episode1.EpisodeId })); + } + + var parameters2 = new DynamicParameters(); + parameters2.Add("series_id", episode2.SeriesId, DbType.UInt64); + parameters2.Add("season_id", episode2.SeasonId, DbType.UInt64); + parameters2.Add("episode_id", episode2.EpisodeId, DbType.UInt64); + parameters2.Add("title", episode2.Title, DbType.String); + parameters2.Add("air_date", episode2.AirDate, DbType.Date); + + await connection.ExecuteAsync($@" +UPSERT INTO {Tables.Episodes} +( + series_id, + season_id, + episode_id, + title, + air_date +) +VALUES +( + @series_id, + @season_id, + @episode_id, + @title, + @air_date +); +;", parameters2, transaction); + await transaction.CommitAsync(); + + var rollbackTransaction = connection.BeginTransaction(); + await connection.ExecuteAsync($@" +INSERT INTO {Tables.Episodes} +( + series_id, + season_id, + episode_id, + title, + air_date +) +VALUES +( + 2, + 5, + 21, + ""Test 21"", + Date(""2018-08-27"") +), -- Rows are separated by commas. +( + 2, + 5, + 22, + ""Test 22"", + Date(""2018-08-27"") +) +; +;", transaction: rollbackTransaction); + await rollbackTransaction.RollbackAsync(); + + Assert.Equal((ulong)72, await connection.ExecuteScalarAsync($"SELECT COUNT(*) FROM {Tables.Episodes}")); + + await connection.ExecuteAsync(Tables.DeleteTables); + } + + private record Episode + { + [Column("series_id")] public uint SeriesId { get; set; } + [Column("season_id")] public uint SeasonId { get; set; } + [Column("episode_id")] public uint EpisodeId { get; set; } + [Column("title")] public string Title { get; set; } = null!; + [Column("air_date")] public DateTime AirDate { get; set; } + } +} diff --git a/src/Ydb.Sdk/tests/Tests.csproj b/src/Ydb.Sdk/tests/Tests.csproj index 1e92b9a6..18e84cda 100644 --- a/src/Ydb.Sdk/tests/Tests.csproj +++ b/src/Ydb.Sdk/tests/Tests.csproj @@ -16,6 +16,7 @@ +