diff --git a/examples/YdbExamples.sln b/examples/YdbExamples.sln index 31c103ee..28c296ae 100644 --- a/examples/YdbExamples.sln +++ b/examples/YdbExamples.sln @@ -27,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Database.Operations.Tutoria EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ydb.Sdk.AdoNet.Yandex.Cloud.Serverless.Container", "Ydb.Sdk.AdoNet.Yandex.Cloud.Serverless.Container\Ydb.Sdk.AdoNet.Yandex.Cloud.Serverless.Container.csproj", "{77625697-498B-4879-BABA-046EE93E7AF7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "linq2db.Ydb.QuickStart", "linq2db.Ydb.QuickStart\linq2db.Ydb.QuickStart.csproj", "{FCB99CC4-F97D-4BDB-AB5F-C74B40F3CE6E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -81,6 +83,10 @@ Global {77625697-498B-4879-BABA-046EE93E7AF7}.Debug|Any CPU.Build.0 = Debug|Any CPU {77625697-498B-4879-BABA-046EE93E7AF7}.Release|Any CPU.ActiveCfg = Release|Any CPU {77625697-498B-4879-BABA-046EE93E7AF7}.Release|Any CPU.Build.0 = Release|Any CPU + {FCB99CC4-F97D-4BDB-AB5F-C74B40F3CE6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCB99CC4-F97D-4BDB-AB5F-C74B40F3CE6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCB99CC4-F97D-4BDB-AB5F-C74B40F3CE6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCB99CC4-F97D-4BDB-AB5F-C74B40F3CE6E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/examples/linq2db.Ydb.QuickStart/Program.cs b/examples/linq2db.Ydb.QuickStart/Program.cs new file mode 100644 index 00000000..f20c0e52 --- /dev/null +++ b/examples/linq2db.Ydb.QuickStart/Program.cs @@ -0,0 +1,360 @@ +using Microsoft.Extensions.Logging; +using Polly; +using LinqToDB; +using LinqToDB.Async; +using LinqToDB.Data; +using LinqToDB.Mapping; + +using var factory = LoggerFactory.Create(b => b.AddConsole()); +await new AppContext(factory.CreateLogger()).Run(); + +#region LINQ2DB MODELS + +[Table("series")] +public sealed class Series +{ + [PrimaryKey, Column("series_id")] + public ulong SeriesId { get; set; } + + [Column("title"), NotNull] + public string Title { get; set; } = null!; + + [Column("series_info")] + public string? SeriesInfo { get; set; } + + [Column("release_date"), DataType(DataType.Date)] + public DateTime ReleaseDate { get; set; } +} + +[Table("seasons")] +public sealed class Season +{ + [PrimaryKey, Column("series_id")] + public ulong SeriesId { get; set; } + + [PrimaryKey, Column("season_id")] + public ulong SeasonId { get; set; } + + [Column("title"), NotNull] + public string Title { get; set; } = null!; + + [Column("first_aired"), DataType(DataType.Date)] + public DateTime FirstAired { get; set; } + + [Column("last_aired"), DataType(DataType.Date)] + public DateTime LastAired { get; set; } +} + +[Table("episodes")] +public sealed class Episode +{ + [PrimaryKey, Column("series_id")] + public ulong SeriesId { get; set; } + + [PrimaryKey, Column("season_id")] + public ulong SeasonId { get; set; } + + [PrimaryKey, Column("episode_id")] + public ulong EpisodeId { get; set; } + + [Column("title"), NotNull] + public string Title { get; set; } = null!; + + [Column("air_date"), DataType(DataType.Date)] + public DateTime AirDate { get; set; } +} + +#endregion + +#region LINQ2DB DATACONTEXT + +internal sealed class MyYdb : DataConnection +{ + public MyYdb(string connectionString) : base("YDB", connectionString) {} + public MyYdb(DataOptions options) : base(options) {} + + public ITable Series => this.GetTable(); + public ITable Seasons => this.GetTable(); + public ITable Episodes => this.GetTable(); +} + +#endregion + +#region SETTINGS (без CmdOptions) + +internal sealed record Settings( + string Host, + int Port, + string Database, + bool UseTls, + int TlsPort) +{ + public string SimpleConnectionString => + $"Host={Host};Port={(UseTls ? TlsPort : Port)};Database={Database};UseTls={(UseTls ? "true" : "false")}"; +} + +internal static class SettingsLoader +{ + public static Settings Load() + { + string host = Environment.GetEnvironmentVariable("YDB_HOST") ?? "localhost"; + int port = TryInt(Environment.GetEnvironmentVariable("YDB_PORT"), 2136); + string db = Environment.GetEnvironmentVariable("YDB_DB") ?? "/local"; + bool useTls = TryBool(Environment.GetEnvironmentVariable("YDB_USE_TLS"), false); + int tls = TryInt(Environment.GetEnvironmentVariable("YDB_TLS_PORT"), 2135); + + return new Settings(host, port, db, useTls, tls); + + static int TryInt(string? s, int d) => int.TryParse(s, out var v) ? v : d; + static bool TryBool(string? s, bool d)=> bool.TryParse(s, out var v) ? v : d; + } +} + +#endregion + +internal class AppContext +{ + private readonly ILogger _logger; + private readonly Settings _settings; + + public AppContext(ILogger logger) + { + _logger = logger; + _settings = SettingsLoader.Load(); + } + + DataOptions BuildOptions(string? overrideConnectionString = null) + { + var cs = overrideConnectionString ?? _settings.SimpleConnectionString; + return new DataOptions().UseConnectionString("YDB", cs); + } + + public async Task Run() + { + _logger.LogInformation("Start app example"); + + await InitTables(); + await LoadData(); + await SelectWithParameters(); + await RetryPolicy(); + + _logger.LogInformation("Clearing all pools..."); + _logger.LogInformation("Cleared all pools"); + + await InteractiveTransaction(); + await TlsConnectionExample(); + await ConnectionWithLoggerFactory(); + + _logger.LogInformation("Finish app example"); + } + + + private async Task InitTables() + { + await using var db = new MyYdb(BuildOptions()); + + try { await db.CreateTableAsync(); } catch { _logger.LogDebug("series exists"); } + try { await db.CreateTableAsync(); } catch { _logger.LogDebug("seasons exists"); } + try { await db.CreateTableAsync(); } catch { _logger.LogDebug("episodes exists"); } + + _logger.LogInformation("Created tables"); + } + + private async Task LoadData() + { + await using var db = new MyYdb(BuildOptions()); + + var series = new[] + { + new Series { SeriesId = 1, Title = "IT Crowd", ReleaseDate = new DateTime(2006,02,03), SeriesInfo="British sitcom..." }, + new Series { SeriesId = 2, Title = "Silicon Valley",ReleaseDate = new DateTime(2014,04,06), SeriesInfo="American comedy..." } + }; + foreach (var s in series) await db.InsertAsync(s); + + var seasons = new List + { + new() { SeriesId=1, SeasonId=1, Title="Season 1", FirstAired=new DateTime(2006,02,03), LastAired=new DateTime(2006,03,03)}, + new() { SeriesId=1, SeasonId=2, Title="Season 2", FirstAired=new DateTime(2007,08,24), LastAired=new DateTime(2007,09,28)}, + new() { SeriesId=1, SeasonId=3, Title="Season 3", FirstAired=new DateTime(2008,11,21), LastAired=new DateTime(2008,12,26)}, + new() { SeriesId=1, SeasonId=4, Title="Season 4", FirstAired=new DateTime(2010,06,25), LastAired=new DateTime(2010,07,30)}, + new() { SeriesId=2, SeasonId=1, Title="Season 1", FirstAired=new DateTime(2014,04,06), LastAired=new DateTime(2014,06,01)}, + new() { SeriesId=2, SeasonId=2, Title="Season 2", FirstAired=new DateTime(2015,04,12), LastAired=new DateTime(2015,06,14)}, + new() { SeriesId=2, SeasonId=3, Title="Season 3", FirstAired=new DateTime(2016,04,24), LastAired=new DateTime(2016,06,26)}, + new() { SeriesId=2, SeasonId=4, Title="Season 4", FirstAired=new DateTime(2017,04,23), LastAired=new DateTime(2017,06,25)}, + new() { SeriesId=2, SeasonId=5, Title="Season 5", FirstAired=new DateTime(2018,03,25), LastAired=new DateTime(2018,05,13)}, + }; + await db.BulkCopyAsync(seasons); + + var eps = new List + { + new() { SeriesId=1, SeasonId=1, EpisodeId=1, Title="Yesterday's Jam", AirDate=new DateTime(2006,02,03)}, + new() { SeriesId=1, SeasonId=1, EpisodeId=2, Title="Calamity Jen", AirDate=new DateTime(2006,02,03)}, + new() { SeriesId=1, SeasonId=1, EpisodeId=3, Title="Fifty-Fifty", AirDate=new DateTime(2006,02,10)}, + new() { SeriesId=1, SeasonId=1, EpisodeId=4, Title="The Red Door", AirDate=new DateTime(2006,02,17)}, + new() { SeriesId=1, SeasonId=2, EpisodeId=1, Title="The Work Outing", AirDate=new DateTime(2007,08,24)}, + new() { SeriesId=1, SeasonId=2, EpisodeId=2, Title="Return of the Golden Child", AirDate=new DateTime(2007,08,31)}, + new() { SeriesId=1, SeasonId=3, EpisodeId=1, Title="From Hell", AirDate=new DateTime(2008,11,21)}, + new() { SeriesId=1, SeasonId=3, EpisodeId=2, Title="Are We Not Men?", AirDate=new DateTime(2008,11,28)}, + new() { SeriesId=1, SeasonId=4, EpisodeId=1, Title="Jen The Fredo", AirDate=new DateTime(2010,06,25)}, + new() { SeriesId=1, SeasonId=4, EpisodeId=2, Title="The Final Countdown", AirDate=new DateTime(2010,07,02)}, + new() { SeriesId=2, SeasonId=2, EpisodeId=1, Title="Minimum Viable Product", AirDate=new DateTime(2014,04,06)}, + new() { SeriesId=2, SeasonId=2, EpisodeId=2, Title="The Cap Table", AirDate=new DateTime(2014,04,13)}, + new() { SeriesId=2, SeasonId=1, EpisodeId=3, Title="Articles of Incorporation", AirDate=new DateTime(2014,04,20)}, + new() { SeriesId=2, SeasonId=1, EpisodeId=4, Title="Fiduciary Duties", AirDate=new DateTime(2014,04,27)}, + }; + + await db.BulkCopyAsync(eps); + + _logger.LogInformation("Loaded data"); + } + + private async Task SelectWithParameters() + { + await using var db = new MyYdb(BuildOptions()); + + ulong seriesId = 1; + ulong seasonId = 1; + ulong limit = 3; + + var rows = await db.Episodes + .Where(e => e.SeriesId == seriesId && e.SeasonId > seasonId) + .OrderBy(e => e.SeriesId) + .ThenBy(e => e.SeasonId) + .ThenBy(e => e.EpisodeId) + .Take((int)limit) + .Select(e => new { e.SeriesId, e.SeasonId, e.EpisodeId, e.AirDate, e.Title }) + .ToListAsync(); + + _logger.LogInformation("Selected rows:"); + foreach (var r in rows) + _logger.LogInformation( + "series_id: {series_id}, season_id: {season_id}, episode_id: {episode_id}, air_date: {air_date}, title: {title}", + r.SeriesId, r.SeasonId, r.EpisodeId, r.AirDate, r.Title); + } + + private async Task RetryPolicy() + { + var policy = Polly.Policy + .Handle(_ => true) + .WaitAndRetryAsync(10, _ => TimeSpan.FromSeconds(1)); + + await policy.ExecuteAsync(async () => + { + await using var db = new MyYdb(BuildOptions()); + + var statsRaw = await db.Episodes + .GroupBy(e => new { e.SeriesId, e.SeasonId }) + .Select(g => new + { + SeriesId = g.Key.SeriesId, + SeasonId = g.Key.SeasonId, + Cnt = g.Count() + }) + .ToListAsync(); + + var stats = statsRaw + .OrderBy(x => x.SeriesId) + .ThenBy(x => x.SeasonId); + + foreach (var x in stats) + _logger.LogInformation("series_id: {series_id}, season_id: {season_id}, cnt: {cnt}", + x.SeriesId, x.SeasonId, x.Cnt); + }); + } + + private async Task InteractiveTransaction() + { + await using var db = new MyYdb(BuildOptions()); + using var tr = await db.BeginTransactionAsync(); + + await db.InsertAsync(new Episode + { + SeriesId = 2, SeasonId = 5, EpisodeId = 13, + Title = "Test Episode", AirDate = new DateTime(2018, 08, 27) + }); + await db.InsertAsync(new Episode + { + SeriesId = 2, SeasonId = 5, EpisodeId = 21, + Title = "Test 21", AirDate = new DateTime(2018, 08, 27) + }); + await db.InsertAsync(new Episode + { + SeriesId = 2, SeasonId = 5, EpisodeId = 22, + Title = "Test 22", AirDate = new DateTime(2018, 08, 27) + }); + + await tr.CommitAsync(); + _logger.LogInformation("Commit transaction"); + + string title21 = await db.Episodes + .Where(e => e.SeriesId == 2 && e.SeasonId == 5 && e.EpisodeId == 21) + .Select(e => e.Title) + .SingleAsync(); + _logger.LogInformation("New episode title: {title}", title21); + + string title22 = await db.Episodes + .Where(e => e.SeriesId == 2 && e.SeasonId == 5 && e.EpisodeId == 22) + .Select(e => e.Title) + .SingleAsync(); + _logger.LogInformation("New episode title: {title}", title22); + + string title13 = await db.Episodes + .Where(e => e.SeriesId == 2 && e.SeasonId == 5 && e.EpisodeId == 13) + .Select(e => e.Title) + .SingleAsync(); + _logger.LogInformation("Updated episode title: {title}", title13); + } + + private async Task TlsConnectionExample() + { + if (!_settings.UseTls) + { + _logger.LogInformation("Tls example was ignored"); + return; + } + + var caPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "ca.pem"); + var tlsCs = $"Host={_settings.Host};Port={_settings.TlsPort};RootCertificate={caPath}"; + await using var db = new MyYdb(BuildOptions(tlsCs)); + + var rows = await db.Seasons + .Where(sa => sa.SeriesId == 1) + .Join(db.Series, sa => sa.SeriesId, sr => sr.SeriesId, + (sa, sr) => new { SeasonTitle = sa.Title, SeriesTitle = sr.Title, sr.SeriesId, sa.SeasonId }) + .OrderBy(x => x.SeriesId).ThenBy(x => x.SeasonId) + .ToListAsync(); + + foreach (var r in rows) + _logger.LogInformation("season_title: {SeasonTitle}, series_title: {SeriesTitle}, series_id: {SeriesId}, season_id: {SeasonId}", + r.SeasonTitle, r.SeriesTitle, r.SeriesId, r.SeasonId); + } + + private async Task ConnectionWithLoggerFactory() + { + await using var db = new MyYdb(BuildOptions( + $"Host={_settings.Host};Port={_settings.Port}")); + + db.OnTraceConnection = ti => + { + switch (ti.TraceInfoStep) + { + case TraceInfoStep.BeforeExecute: + _logger.LogInformation("BeforeExecute: {sql}", ti.SqlText); + break; + case TraceInfoStep.AfterExecute: + _logger.LogInformation("AfterExecute: {time} {records} recs", ti.ExecutionTime, ti.RecordsAffected); + break; + case TraceInfoStep.Error: + _logger.LogError(ti.Exception, "SQL error"); + break; + } + }; + + _logger.LogInformation("Dropping tables of examples"); + try { await db.DropTableAsync(); } catch { /* ignore */ } + try { await db.DropTableAsync(); } catch { /* ignore */ } + try { await db.DropTableAsync(); } catch { /* ignore */ } + _logger.LogInformation("Dropped tables of examples"); + } +} diff --git a/examples/linq2db.Ydb.QuickStart/linq2db.Ydb.QuickStart.csproj b/examples/linq2db.Ydb.QuickStart/linq2db.Ydb.QuickStart.csproj new file mode 100644 index 00000000..687ad40c --- /dev/null +++ b/examples/linq2db.Ydb.QuickStart/linq2db.Ydb.QuickStart.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + +