From 242d7b647b5d057a4c08c54bf11425ded6743993 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Sun, 23 Jun 2024 00:27:29 +0900 Subject: [PATCH] Added a demo of aspect-oriented SQL processing. --- .../Demo.AOPFiltering.csproj} | 0 demo/AOPFiltering/Program.cs | 209 ++++++++++++++++++ demo/TypeSafeBuild/Demo.TypeSafeBuild.csproj | 14 ++ src/Carbunql.TypeSafe/SelectQueryExtension.cs | 110 +++++++++ src/Carbunql.sln | 16 +- src/Carbunql/Clauses/ValueBase.cs | 2 + 6 files changed, 346 insertions(+), 5 deletions(-) rename demo/{TypeSafeBuild/TypeSafeBuild.csproj => AOPFiltering/Demo.AOPFiltering.csproj} (100%) create mode 100644 demo/AOPFiltering/Program.cs create mode 100644 demo/TypeSafeBuild/Demo.TypeSafeBuild.csproj create mode 100644 src/Carbunql.TypeSafe/SelectQueryExtension.cs diff --git a/demo/TypeSafeBuild/TypeSafeBuild.csproj b/demo/AOPFiltering/Demo.AOPFiltering.csproj similarity index 100% rename from demo/TypeSafeBuild/TypeSafeBuild.csproj rename to demo/AOPFiltering/Demo.AOPFiltering.csproj diff --git a/demo/AOPFiltering/Program.cs b/demo/AOPFiltering/Program.cs new file mode 100644 index 00000000..3c120054 --- /dev/null +++ b/demo/AOPFiltering/Program.cs @@ -0,0 +1,209 @@ +using Carbunql; +using Carbunql.TypeSafe; +using Carbunql.Building; + +internal class Program +{ + private static void Main(string[] args) + { + var staffId = 1; + + //全件取得 + Console.WriteLine(SelectAreaSaleReport().ToText()); + Console.WriteLine(";"); + + Console.WriteLine(SelectAreaSaleReportByStaffId(staffId).ToText()); + Console.WriteLine(";"); + + //帳票別フィルタリング + Console.WriteLine(SelectStoreSaleReport().ToText()); + Console.WriteLine(";"); + + Console.WriteLine(SelectStoreSaleReportByStaffId(staffId).ToText()); + Console.WriteLine(";"); + + //アスペクト指向フィルタリング + Console.WriteLine(SelectAreaSaleReport().FilterByStaffId(staffId).ToText()); + Console.WriteLine(";"); + + Console.WriteLine(SelectStoreSaleReport().FilterByStaffId(staffId).ToText()); + Console.WriteLine(";"); + } + + /// + /// Area_sale_reportを全件取得します + /// + /// + private static FluentSelectQuery SelectAreaSaleReport() + { + var r = Sql.DefineDataSet(); + return Sql.From(() => r).Select(() => r).Compile(); + } + + /// + /// Area_sale_reportをstaff_idでフィルタリングします + /// + /// + /// + private static FluentSelectQuery SelectAreaSaleReportByStaffId(long staffId) + { + var r = Sql.DefineDataSet(() => SelectAreaSaleReport()); + return Sql.From(() => r) + .Where(() => r.sales_staff_id == staffId); + } + + /// + /// Store_sale_reportを全件取得します + /// + /// + private static FluentSelectQuery SelectStoreSaleReport() + { + var r = Sql.DefineDataSet(); + return Sql.From(() => r).Select(() => r).Compile(); + } + + /// + /// スタッフとストアの関連を示すテーブルを取得します + /// (おおげさ) + /// + /// + private static FluentSelectQuery SelectAreaDetailWithStaff() + { + var a = Sql.DefineDataSet(); + var d = Sql.DefineDataSet(); + return Sql.From(() => d) + .InnerJoin(() => a, () => d.area_id == a.area_id) + .Select(() => d) + .Select(() => new + { + a.sales_staff_id + }).Compile(); + } + + /// + /// Store_sale_reportをstaff_idでフィルタリングします + /// + /// + /// + private static FluentSelectQuery SelectStoreSaleReportByStaffId(long staffId) + { + var r = Sql.DefineDataSet(() => SelectStoreSaleReport()); + return Sql.From(() => r) + .Exists(SelectAreaDetailWithStaff, x => r.store_id == x.store_id && x.sales_staff_id == staffId); + } +} + +public static class AspectOrientedFiltering +{ + /// + /// 選択クエリをスタッフIDでフィルタリングします。 + /// フィルタリングできない場合は例外が発生します + /// + /// + /// + /// + /// + /// + public static FluentSelectQuery FilterByStaffId(this FluentSelectQuery query, long staffId) where T : IDataRow, new() + { + // 列「sales_staff_id」が存在する場合、該当列でフィルタする + var sales_staff_id = query.SelectClause!.Where(x => x.Alias == "sales_staff_id").FirstOrDefault()?.Alias; + if (sales_staff_id != null) + { + //タイプセーフではないビルド + //引数のクエリをサブクエリqとして定義する + var sq = new SelectQuery(); + var (f, q) = sq.From(query).As("q"); + sq.SelectAll(q); + + //列「sales_staff_id」を検索条件にする + sq.Where(q, sales_staff_id).Equal(sq.AddParameter(":staff_id", staffId)); + + //コメントを足す + sq.AddComment(nameof(FilterByStaffId)); + + //タイプセーフに戻す + return sq.Compile(); + } + + // 列「store_id」が存在する場合、areaテーブル経由でフィルタする + var store_id = query.SelectClause!.Where(x => x.Alias == "store_id").FirstOrDefault()?.Alias; + if (store_id != null) + { + //タイプセーフではないビルド + //引数のクエリをサブクエリqとして定義する + var sq = new SelectQuery(); + var (_, q) = sq.From(query).As("q"); + sq.SelectAll(q); + + //列「store_id」を検索条件にする + sq.Where(() => + { + // area をstaff_id でフィルタし、 + // area_detail と結合して、store_id に展開する + var xsq = new SelectQuery(); + var (f, d) = xsq.From("area_detail").As("d"); + var a = f.InnerJoin("area").As("a").On(d, "area_id"); + xsq.Where(a, "sales_staff_id").Equal(xsq.AddParameter(":staff_id", staffId)); + xsq.Where(q, store_id).Equal(d, store_id); + return xsq.ToExists(); + }); + + //コメントを足す + sq.AddComment($"{nameof(FilterByStaffId)}, column:store_id"); + + //タイプセーフに戻す + return sq.Compile(); + } + + throw new NotImplementedException(); + } +} + +public record Area : IDataRow +{ + public long area_id { get; set; } + + public long area_name { get; set; } + + public long sales_staff_id { get; set; } + + public IDataSet DataSet { get; set; } = null!; +} + +public record Area_detail : IDataRow +{ + public long area_id { get; set; } + + public long store_id { get; set; } + + public IDataSet DataSet { get; set; } = null!; +} + +public record Area_detail_with_staff : IDataRow +{ + public long area_id { get; set; } + + public long store_id { get; set; } + + public long sales_staff_id { get; set; } + + public IDataSet DataSet { get; set; } = null!; +} + +public record Area_sale_report : IDataRow +{ + public long area_id { get; set; } + public long sales_staff_id { get; set; } + public decimal sale_price { get; set; } + + public IDataSet DataSet { get; set; } = null!; +} + +public record Store_sale_report : IDataRow +{ + public long store_id { get; set; } + public decimal sale_price { get; set; } + + public IDataSet DataSet { get; set; } = null!; +} \ No newline at end of file diff --git a/demo/TypeSafeBuild/Demo.TypeSafeBuild.csproj b/demo/TypeSafeBuild/Demo.TypeSafeBuild.csproj new file mode 100644 index 00000000..1f9ab766 --- /dev/null +++ b/demo/TypeSafeBuild/Demo.TypeSafeBuild.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/src/Carbunql.TypeSafe/SelectQueryExtension.cs b/src/Carbunql.TypeSafe/SelectQueryExtension.cs new file mode 100644 index 00000000..83402384 --- /dev/null +++ b/src/Carbunql.TypeSafe/SelectQueryExtension.cs @@ -0,0 +1,110 @@ +using Carbunql.Annotations; +using Carbunql.Building; +using Carbunql.Definitions; +using Carbunql.Tables; +using System.Data; + +namespace Carbunql.TypeSafe; + +public static class SelectQueryExtension +{ + + public static FluentSelectQuery Compile(this SelectQuery source, bool force = false) where T : IDataRow, new() + { + var q = new FluentSelectQuery(); + + // Copy clauses and parameters to the new query object + q.WithClause = source.WithClause; + q.SelectClause = source.SelectClause; + q.FromClause = source.FromClause; + q.WhereClause = source.WhereClause; + q.GroupClause = source.GroupClause; + q.HavingClause = source.HavingClause; + q.WindowClause = source.WindowClause; + q.OperatableQueries = source.OperatableQueries; + q.OrderClause = source.OrderClause; + q.LimitClause = source.LimitClause; + q.Parameters = source.Parameters; + q.CommentClause = source.CommentClause; + q.HeaderCommentClause = source.HeaderCommentClause; + + var columns = PropertySelector.SelectLiteralProperties().Select(x => x.Name); + + if (force) + { + foreach (var item in columns) + { + q.Select(q.FromClause!.Root, item); + } + } + + TypeValidate(q, columns); + + if (q.SelectClause == null) + { + foreach (var item in columns) + { + q.Select(q.FromClause!.Root, item); + } + }; + + return q; + } + + private static void TypeValidate(SelectQuery q) + { + TypeValidate(q, PropertySelector.SelectLiteralProperties().Select(x => x.Name)); + } + + private static void TypeValidate(SelectQuery q, IEnumerable columns) + { + if (q.SelectClause != null && !(q.SelectClause.Count == 1 && q.SelectClause[0].Alias == "*")) + { + // Check if all properties of T are specified in the select clause + var aliases = q.GetSelectableItems().Select(x => x.Alias).ToHashSet(); + var missingColumns = columns.Where(item => !aliases.Contains(item)).ToList(); + + if (missingColumns.Any()) + { + // If there are missing columns, include all of them in the error message + throw new InvalidProgramException($"The select query is not compatible with '{typeof(T).Name}'. The following columns are missing: {string.Join(", ", missingColumns)}"); + } + return; + } + else if (q.FromClause != null) + { + if (q.FromClause.Root.Table is VirtualTable v && v.Query is SelectQuery vq) + { + TypeValidate(vq, columns); + return; + } + else if (q.FromClause.Root.Table is CTETable ct) + { + // Check if all properties of T are specified in the select clause + var aliases = ct.GetColumnNames().ToHashSet(); + var missingColumns = columns.Where(item => !aliases.Contains(item)).ToList(); + + if (missingColumns.Any()) + { + // If there are missing columns, include all of them in the error message + throw new InvalidProgramException($"The select query is not compatible with '{typeof(T).Name}'. The following columns are missing: {string.Join(", ", missingColumns)}"); + } + return; + } + + var actual = q.FromClause.Root.Table.GetTableFullName(); + var clause = TableDefinitionClauseFactory.Create(); + var expect = clause.GetTableFullName(); + + if (!actual.Equals(expect)) + { + throw new InvalidProgramException($"The select query is not compatible with '{typeof(T).Name}'. Expect: {expect}, Actual: {actual}"); + } + return; + } + else + { + throw new InvalidProgramException($"The select query is not compatible with '{typeof(T).Name}'. FromClause is null."); + } + } +} diff --git a/src/Carbunql.sln b/src/Carbunql.sln index 864eacde..637a21d0 100644 --- a/src/Carbunql.sln +++ b/src/Carbunql.sln @@ -33,7 +33,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Carbunql.TypeSafe", "Carbun EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Carbunql.TypeSafe.Test", "..\test\Carbunql.TypeSafe.Test\Carbunql.TypeSafe.Test.csproj", "{EA3034E3-564E-46B4-9FA8-B0A10D9DCBCE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TypeSafeBuild", "..\demo\TypeSafeBuild\TypeSafeBuild.csproj", "{680F4563-4F6A-454C-B769-5FF7F768B7FF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.AOPFiltering", "..\demo\AOPFiltering\Demo.AOPFiltering.csproj", "{63CE8E1D-71A9-4ACE-AE52-613B99394060}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.TypeSafeBuild", "..\demo\TypeSafeBuild\Demo.TypeSafeBuild.csproj", "{B1016F33-6565-4A6E-844E-EF966E54F862}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -101,10 +103,14 @@ Global {EA3034E3-564E-46B4-9FA8-B0A10D9DCBCE}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA3034E3-564E-46B4-9FA8-B0A10D9DCBCE}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA3034E3-564E-46B4-9FA8-B0A10D9DCBCE}.Release|Any CPU.Build.0 = Release|Any CPU - {680F4563-4F6A-454C-B769-5FF7F768B7FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {680F4563-4F6A-454C-B769-5FF7F768B7FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {680F4563-4F6A-454C-B769-5FF7F768B7FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {680F4563-4F6A-454C-B769-5FF7F768B7FF}.Release|Any CPU.Build.0 = Release|Any CPU + {63CE8E1D-71A9-4ACE-AE52-613B99394060}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63CE8E1D-71A9-4ACE-AE52-613B99394060}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63CE8E1D-71A9-4ACE-AE52-613B99394060}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63CE8E1D-71A9-4ACE-AE52-613B99394060}.Release|Any CPU.Build.0 = Release|Any CPU + {B1016F33-6565-4A6E-844E-EF966E54F862}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1016F33-6565-4A6E-844E-EF966E54F862}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1016F33-6565-4A6E-844E-EF966E54F862}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1016F33-6565-4A6E-844E-EF966E54F862}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Carbunql/Clauses/ValueBase.cs b/src/Carbunql/Clauses/ValueBase.cs index 196d7d53..7996b058 100644 --- a/src/Carbunql/Clauses/ValueBase.cs +++ b/src/Carbunql/Clauses/ValueBase.cs @@ -25,6 +25,8 @@ namespace Carbunql.Clauses; [Union(12, typeof(QueryContainer))] [Union(13, typeof(ValueCollection))] [Union(14, typeof(Interval))] +[Union(15, typeof(LikeClause))] +[Union(16, typeof(ExistsExpression))] public abstract class ValueBase : IQueryCommandable { ///