Parcourir la source

Converted StockHolding into an AutoEntitySummary Table

frankvandenbos il y a 1 mois
Parent
commit
75e82095fb

+ 150 - 0
InABox.Core/Objects/AutoEntity/AutoEntitySummaryGenerator.cs

@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Runtime.CompilerServices;
+
+namespace InABox.Core
+{
+    /*
+     * Heres what we want to model - a view that performs in-table summaries.  Currently we can define out-of-table lookups that sumamrise the values in another table,
+     * but these all seem to generate multi-table queeries, which lose the performance benefits we're looking for.  The below "target" query is a single-table query
+     *
+	    select SUMMARYTABLE.* from 
+			(SELECT 
+			  [location.id],  
+			  [product.id],  
+			  [style.id],  
+			  [job.id],  
+			  [dimensions.unit.id], 
+			  [dimensions.Quantity],  
+			  [dimensions.length],  
+			  [dimensions.width],  
+			  [dimensions.Height],  
+			  [dimensions.weight],  
+			  [dimensions.value],  
+			  [dimensions.unitsize],
+			  sum(coalesce([RECEIVED],0.0)-coalesce([ISSUED],0.0)) AS Total,
+			  sum(case when [JOBREQUISITIONITEM.ID] is null THEN coalesce([RECEIVED],0.0)-coalesce([ISSUED],0.0) ELSE 0.0 end) AS Available,
+			  sum(case when [JOBREQUISITIONITEM.ID] is not null THEN coalesce([RECEIVED],0.0)-coalesce([ISSUED],0.0) ELSE 0.0 end) AS Allocated,
+			  sum((coalesce([RECEIVED],0.0)-coalesce([ISSUED],0.0)) * coalesce(COST,0.0)) AS TotalCost,
+			  sum((coalesce([RECEIVED],0.0)-coalesce([ISSUED],0.0)) * coalesce(COST,0.0)) / sum(coalesce([RECEIVED],0.0)-coalesce([ISSUED],0.0)) AS AverageCost
+			FROM 
+			  STOCKMOVEMENT
+			GROUP BY
+			  [location.id],  
+			  [product.id],  
+			  [style.id],  
+			  [job.id],  
+			  [dimensions.unit.id], 
+			  [dimensions.Quantity],  
+			  [dimensions.length],  
+			  [dimensions.width],  
+			  [dimensions.Height],  
+			  [dimensions.weight],  
+			  [dimensions.value],  
+			  [dimensions.unitsize]
+			  ) as SUMMARYTABLE
+		WHERE
+			SUMMARYTABLE.[PRODUCT.ID]='fc159c37-6a59-410b-b15c-8baed75349aa'
+
+     */
+
+    public enum AutoEntitySummaryAggregate
+    {
+	    Sum,
+	    Average,
+	    Count,
+	    Minimum,
+	    Maximum
+    }
+    
+    public interface IAutoEntitySummaryAggregateColumn : IColumn
+    {
+	    AutoEntitySummaryAggregate Aggregate { get; }
+	    IComplexFormulaGenerator Definition { get; }
+    }
+    
+    public class AutoEntitySummaryAggregateColumn<TInterface,TType> : Column<TInterface>, IAutoEntitySummaryAggregateColumn
+    {
+	    
+	    public AutoEntitySummaryAggregate Aggregate { get; }
+	    
+	    public ComplexFormulaGenerator<TType,object?> Definition { get; }
+	    IComplexFormulaGenerator IAutoEntitySummaryAggregateColumn.Definition => Definition;
+	    
+	    public AutoEntitySummaryAggregateColumn(Expression<Func<TInterface, object?>> expression, AutoEntitySummaryAggregate aggregate, ComplexFormulaGenerator<TType,object?> definition) : base(expression)
+	    {
+		    Aggregate = aggregate;
+		    Definition = definition;
+	    }
+	    
+    }
+
+    public interface IAutoEntitySummaryIDColumn : IColumn
+    {
+	    IColumn Source { get; }
+    }
+    
+    public class AutoEntitySummaryIDColumn<TInterface,TType> : Column<TInterface>, IAutoEntitySummaryIDColumn
+    {
+	    
+	    public Column<TType> Source { get; }
+	    
+	    IColumn IAutoEntitySummaryIDColumn.Source => Source;
+	    
+	    public AutoEntitySummaryIDColumn(Expression<Func<TInterface, object?>> expression, Expression<Func<TType, object?>> source) : base(expression)
+	    {
+		    Source = new Column<TType>(source);
+	    }
+	    
+    }
+
+    public interface IAutoEntitySummaryGenerator : IAutoEntityGenerator
+    {
+		new IAutoEntitySummaryIDColumn[] IDColumns();
+	    IAutoEntitySummaryAggregateColumn[] AggregateColumns();
+	    IFilter? HavingFilter { get; }
+	    Type SourceType { get; }
+    }
+
+    
+    public abstract class AutoEntitySummaryGenerator<TInterface,TType> : IAutoEntitySummaryGenerator
+    {
+	    public AutoEntitySummaryGenerator()
+	    {
+		    Configure();
+	    }
+	    
+	    public bool Distinct => false;
+	    
+	    public Type Definition => typeof(TInterface);
+	    public Type SourceType => typeof(TType);
+
+	    private List<IColumn> _idColumns = new List<IColumn>();
+	    private List<IAutoEntitySummaryAggregateColumn> _aggregateColumns = new List<IAutoEntitySummaryAggregateColumn>();
+	    private Filter<TInterface>? _filter = null;
+	    
+	    public abstract void Configure();
+	    
+	    protected void GroupBy(Expression<Func<TInterface,object?>> column, Expression<Func<TType,object?>> source) 
+		    => _idColumns.Add(new AutoEntitySummaryIDColumn<TInterface,TType>(column,source));
+	    
+	    protected void Aggregate(Expression<Func<TInterface,object?>> column,AutoEntitySummaryAggregate aggregate, ComplexFormulaGenerator<TType,object?> formula) 
+		    => _aggregateColumns.Add(new AutoEntitySummaryAggregateColumn<TInterface,TType>(column,aggregate, formula));
+
+	    protected void Having(Filter<TInterface> filter)
+		    => _filter = filter;
+	    
+	    public IAutoEntitySummaryIDColumn[] IDColumns() => _idColumns.OfType<IAutoEntitySummaryIDColumn>().ToArray();
+	    public IAutoEntitySummaryAggregateColumn[] AggregateColumns() => _aggregateColumns.ToArray();
+	    public IFilter? HavingFilter => _filter;
+	    
+	    IColumn[] IAutoEntityGenerator.IDColumns => _idColumns.ToArray();
+
+	    
+    }
+
+
+    
+}

+ 61 - 2
inabox.database.sqlite/SQLiteProvider.cs

@@ -1005,7 +1005,66 @@ public class SQLiteProviderFactory : IProviderFactory
                     sb.Append($" WHERE {String.Join(" AND ", filters)}");
                 ddl.Add(sb.ToString());
             }
+            else if (view.Generator is IAutoEntitySummaryGenerator summary)
+            {
+                // These things dont have the normal columns that an entity requires, so we have to provide default ones
+                List<String> allfields = new List<string>()
+                {
+                    $"NULL as [ID]",
+                    $"NULL as [Created]",
+                    $"NULL as [CreatedBy]",
+                    $"NULL as [LastUpdate]",
+                    $"NULL as [LastUpdateBy]"
+                };
+                List<String> groupfields = new List<string>();
+                string having = "";
+                
+                foreach (var idcol in summary.IDColumns())
+                {
+                    allfields.Add($"A1.[{idcol.Source}] as [{idcol.Property}]");
+                    groupfields.Add($"A1.[{idcol.Source}]");
+                }
+                
+                using (var command = access.CreateCommand())
+                {
+                    var fieldmap = new Dictionary<string, string>();
+                    var tables = new List<Tuple<string, string, string, string>> ();
+                    var columns = new List<string>();
+
+                    foreach (var aggCol in summary.AggregateColumns())
+                    {
+                        var formula = MainProvider.LoadComplexFormula(command, summary.SourceType, 'A', fieldmap, tables, columns,
+                            aggCol.Definition.GetFormula(), false);
+                        // formula already has brackets around it, so the apparent typo is correct
+                        var func = aggCol.Aggregate == AutoEntitySummaryAggregate.Sum
+                            ? $"sum{formula}"
+                            : aggCol.Aggregate == AutoEntitySummaryAggregate.Average
+                                ? $"avg{formula}"
+                                : aggCol.Aggregate == AutoEntitySummaryAggregate.Count
+                                    ? $"count{formula}"
+                                    : aggCol.Aggregate == AutoEntitySummaryAggregate.Minimum
+                                        ? $"min{formula}"
+                                        : aggCol.Aggregate == AutoEntitySummaryAggregate.Maximum
+                                            ? $"max{formula}"
+                                            : formula;
+                        allfields.Add($"{func} as [{aggCol.Property}]");
+                    }
 
+                    if (summary.HavingFilter != null)
+                    {
+                        having = MainProvider.GetFilterClauseNonGeneric(summary.Definition, command, 'A',
+                            summary.HavingFilter, tables, fieldmap, columns, false);
+                        // yep it would be cool to be able to have a non-prefixed "having" clause
+                        // But that's a fair bit of work, so I'm just gonna leave this here for 
+                        // another time
+                        having = having.Replace("A1.", "");
+                    }
+                }
+                var sql = $"SELECT\n  {String.Join(",\n  ", allfields)} \nFROM \n  {summary.SourceType.Name.Split('.').Last()} A1 \nGROUP BY\n  {String.Join(",\n  ", groupfields)}";
+                if (!string.IsNullOrWhiteSpace(having))
+                    sql += $"\nHAVING \n  {having}";
+                ddl.Add(sql);
+            }
 
             ddl.Add(";");
             var viewstatement = string.Join(" ", ddl);
@@ -1872,7 +1931,7 @@ public class SQLiteProvider : IProvider
         };
     }
 
-    private string GetFilterClauseNonGeneric(Type T, SQLiteCommand command, char prefix, IFilter? filter, List<Tuple<string, string, string, string>> tables,
+    public string GetFilterClauseNonGeneric(Type T, SQLiteCommand command, char prefix, IFilter? filter, List<Tuple<string, string, string, string>> tables,
         Dictionary<string, string> fieldmap, List<string> columns, bool useparams)
     {
         if (filter == null)
@@ -2230,7 +2289,7 @@ public class SQLiteProvider : IProvider
             _right, _true, _false);
     }
 
-    private string LoadComplexFormula(SQLiteCommand command, Type type, char prefix, Dictionary<string, string> fieldmap, List<Tuple<string, string, string, string>> tables, List<string> columns, IComplexFormulaNode node, bool useparams)
+    public string LoadComplexFormula(SQLiteCommand command, Type type, char prefix, Dictionary<string, string> fieldmap, List<Tuple<string, string, string, string>> tables, List<string> columns, IComplexFormulaNode node, bool useparams)
     {
         switch(node)
         {