Skip to content

Commit

Permalink
Merge pull request #458 from mk3008/457-creating-an-aspect-oriented-s…
Browse files Browse the repository at this point in the history
…ql-processing-demo

Added a demo of aspect-oriented SQL processing.
  • Loading branch information
mk3008 authored Jun 22, 2024
2 parents fcbf90e + 242d7b6 commit fd3fe87
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 5 deletions.
File renamed without changes.
209 changes: 209 additions & 0 deletions demo/AOPFiltering/Program.cs
Original file line number Diff line number Diff line change
@@ -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(";");
}

/// <summary>
/// Area_sale_reportを全件取得します
/// </summary>
/// <returns></returns>
private static FluentSelectQuery<Area_sale_report> SelectAreaSaleReport()
{
var r = Sql.DefineDataSet<Area_sale_report>();
return Sql.From(() => r).Select(() => r).Compile<Area_sale_report>();
}

/// <summary>
/// Area_sale_reportをstaff_idでフィルタリングします
/// </summary>
/// <param name="staffId"></param>
/// <returns></returns>
private static FluentSelectQuery<Area_sale_report> SelectAreaSaleReportByStaffId(long staffId)
{
var r = Sql.DefineDataSet(() => SelectAreaSaleReport());
return Sql.From(() => r)
.Where(() => r.sales_staff_id == staffId);
}

/// <summary>
/// Store_sale_reportを全件取得します
/// </summary>
/// <returns></returns>
private static FluentSelectQuery<Store_sale_report> SelectStoreSaleReport()
{
var r = Sql.DefineDataSet<Store_sale_report>();
return Sql.From(() => r).Select(() => r).Compile<Store_sale_report>();
}

/// <summary>
/// スタッフとストアの関連を示すテーブルを取得します
/// (おおげさ)
/// </summary>
/// <returns></returns>
private static FluentSelectQuery<Area_detail_with_staff> SelectAreaDetailWithStaff()
{
var a = Sql.DefineDataSet<Area>();
var d = Sql.DefineDataSet<Area_detail>();
return Sql.From(() => d)
.InnerJoin(() => a, () => d.area_id == a.area_id)
.Select(() => d)
.Select(() => new
{
a.sales_staff_id
}).Compile<Area_detail_with_staff>();
}

/// <summary>
/// Store_sale_reportをstaff_idでフィルタリングします
/// </summary>
/// <param name="staffId"></param>
/// <returns></returns>
private static FluentSelectQuery<Store_sale_report> 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
{
/// <summary>
/// 選択クエリをスタッフIDでフィルタリングします。
/// フィルタリングできない場合は例外が発生します
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="staffId"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public static FluentSelectQuery<T> FilterByStaffId<T>(this FluentSelectQuery<T> 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<T>();
}

// 列「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<T>();
}

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!;
}
14 changes: 14 additions & 0 deletions demo/TypeSafeBuild/Demo.TypeSafeBuild.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Carbunql.TypeSafe\Carbunql.TypeSafe.csproj" />
</ItemGroup>

</Project>
110 changes: 110 additions & 0 deletions src/Carbunql.TypeSafe/SelectQueryExtension.cs
Original file line number Diff line number Diff line change
@@ -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<T> Compile<T>(this SelectQuery source, bool force = false) where T : IDataRow, new()
{
var q = new FluentSelectQuery<T>();

// 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<T>().Select(x => x.Name);

if (force)
{
foreach (var item in columns)
{
q.Select(q.FromClause!.Root, item);
}
}

TypeValidate<T>(q, columns);

if (q.SelectClause == null)
{
foreach (var item in columns)
{
q.Select(q.FromClause!.Root, item);
}
};

return q;
}

private static void TypeValidate<T>(SelectQuery q)
{
TypeValidate<T>(q, PropertySelector.SelectLiteralProperties<T>().Select(x => x.Name));
}

private static void TypeValidate<T>(SelectQuery q, IEnumerable<string> 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<T>(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<T>();
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.");
}
}
}
16 changes: 11 additions & 5 deletions src/Carbunql.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/Carbunql/Clauses/ValueBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
Expand Down

0 comments on commit fd3fe87

Please sign in to comment.