Skip to content

Commit

Permalink
Merge pull request #495 from TortugaResearch/Chain-4.3
Browse files Browse the repository at this point in the history
Chain 4.3
  • Loading branch information
Grauenwolf committed Jul 14, 2022
2 parents 20383f1 + 5f7b502 commit cd711d4
Show file tree
Hide file tree
Showing 3,358 changed files with 795,906 additions and 7,514 deletions.
The diff you're trying to view is too large. We only load the first 3000 changed files.
117 changes: 117 additions & 0 deletions Tortuga.Chain/Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,120 @@
## Version 4.3

Updated dependency to Anchor 4.1.

### Features

[#88 Simple Aggregators](https://github.com/TortugaResearch/Tortuga.Chain/issues/88)

The "simple aggregators" agument the `AsCount` method. Each returns a single value for the desired column.

* `AsAverage(columnName)`
* `AsMax(columnName)`
* `AsMin(columnName)`
* `AsSum(columnName, distinct)`

These all return a `ScalarDbCommandBuilder` with which the caller can specify the return type. They are built on the `AggregateColumn` model, which overrides the usual column selection process.

For more complex aggregation, use the `AsAggregate` method. This accepts a collection of `AggregateColumn` objects, which can be used for both aggregegate functions and grouping.

The original `AsCount` methods were reworked to fit into this model.

[#89 Declarative Aggregators](https://github.com/TortugaResearch/Tortuga.Chain/issues/89)

Attributes can now be used to declare aggregations directly in a model.

```csharp
[Table("Sales.EmployeeSalesView"]
public class SalesFigures
{
[AggregateColumn(AggregateType.Min, "TotalPrice")]
public decimal SmallestSale { get; set; }

[AggregateColumn(AggregateType.Max, "TotalPrice")]
public decimal LargestSale { get; set; }

[AggregateColumn(AggregateType.Average, "TotalPrice")]
public decimal AverageSale { get; set; }

[CustomAggregateColumn("Max(TotalPrice) - Min(TotalPrice)")]
public decimal Range { get; set; }

[GroupByColumn]
public int EmployeeKey { get; set; }

[GroupByColumn]
public string EmployeeName { get; set; }
}
```

To use this feature, you need use either of these patterns:

```csharp
datasource.FromTable(tableName, filter).ToAggregate<TObject>().ToCollection().Execute();
datasource.FromTable<TObject>(filter).ToAggregate().ToCollection().Execute();
```

In the second version, the table or view name is extracted from the class.


[#92 ToObjectStream](https://github.com/TortugaResearch/Tortuga.Chain/issues/92)
Previously, Chain would fully manage database connections by default. Specifically, it would open and close connections automatically unless a transaction was involved. In that case, the developer only needed to manage the transactional data source itself.

However, there are times when a result set is too large to handle at one time. In this case the developer will want an `IEnumerable` or `IAsyncEnumerable` instead of a collection. To support this, the `ToObjectStream` materializer was created.

When used in place of `ToCollection`, the caller gets a `ObjectStream` object. This object implements `IEnumerable<TObject>`, `IDisposable`, `IAsyncDisposable`, abd `IAsyncEnumerable<TObject>`. (That latter two are only available in .NET 6 or later.)

This object stream may be used directly, as shown below, or attached to an RX Observable or TPL Dataflow just like any other enumerable data structure.

```csharp
//sync pattern
using var objectStream = dataSource.From<Employee>(new { Title = uniqueKey }).ToObjectStream<Employee>().Execute();
foreach (var item in objectStream)
{
Assert.AreEqual(uniqueKey, item.Title);
}

//async pattern
await using var objectStream = await dataSource.From<Employee>(new { Title = uniqueKey }).ToObjectStream<Employee>().ExecuteAsync();
await foreach (var item in objectStream)
{
Assert.AreEqual(uniqueKey, item.Title);
}
```
It is vital that the object stream is disposed after use. If that doesn't occur, the database can suffer from thread exhaustion or deadlocks.


[#98 Dynamic Materializers and Desired Columns](https://github.com/TortugaResearch/Tortuga.Chain/issues/98)
Allow the use of `WithProperties` or `ExcludeProperties` to be used with...

* `.ToDynamicObject`
* `.ToDynamicObjectOrNull`
* `.ToDynamicCollection`


### Bugs

[#490 Command Timeout is not being honored in PostgreSQL and MySQL](https://github.com/TortugaResearch/Tortuga.Chain/issues/490)
See the ticket for an explaination for why this was broken.

### Technical Debt

[#488 Add IAsyncDisposable support](https://github.com/TortugaResearch/Tortuga.Chain/issues/488)
Added support for `IAsyncDisposable` to transactional data sources.








## Version 4.2

### Features
Expand Down
7 changes: 7 additions & 0 deletions Tortuga.Chain/Engineering Notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ When used...
<AbstractConnection, AbstractTransaction, AbstractCommand, AbstractParameter, AbstractObjectName, AbstractDbType>
```

## Aggregates

Standard aggregate functions are listed in `AggregateType`.

To convert the enum to a database specific function, use `DatabaseMetadataCache.GetAggregateFunction`.

If the database doesn't support a given aggregation, override `GetAggregateFunction` and throw a `NotSupportedException`.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
namespace Traits;

[Trait]
class SupportsUpdateSet<TCommand, TParameter, TObjectName, TDbType> : ISupportsUpdateSet
class SupportsUpdateSetTrait<TCommand, TParameter, TObjectName, TDbType> : ISupportsUpdateSet
where TCommand : DbCommand
where TParameter : DbParameter
where TObjectName : struct
Expand All @@ -16,7 +16,6 @@ class SupportsUpdateSet<TCommand, TParameter, TObjectName, TDbType> : ISupportsU
[Container(RegisterInterface = true)]
internal IUpdateDeleteSetHelper<TCommand, TParameter, TObjectName, TDbType> DataSource { get; set; } = null!;


IUpdateSetDbCommandBuilder ISupportsUpdateSet.UpdateSet(string tableName, string updateExpression, UpdateOptions options)
{
return DataSource.OnUpdateSet(DataSource.DatabaseMetadata.ParseObjectName(tableName), updateExpression, options);
Expand Down Expand Up @@ -76,8 +75,6 @@ IUpdateSetDbCommandBuilder ISupportsUpdateSet.UpdateSet(string tableName, object
return DataSource.OnUpdateSet(tableName, updateExpression, null, options);
}



/// <summary>
/// Update multiple records using an update expression.
/// </summary>
Expand Down Expand Up @@ -119,10 +116,4 @@ IUpdateSetDbCommandBuilder ISupportsUpdateSet.UpdateSet(string tableName, object
{
return DataSource.OnUpdateSet(DataSource.DatabaseMetadata.GetTableOrViewFromClass<TObject>().Name, updateExpression, null, options);
}



}



Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ namespace Traits
{
[Trait]
class TransactionalDataSourceTrait<TRootDataSource, TConnection, TTransaction, TCommand, TDatabaseMetadata> : ITransactionalDataSource, IDisposable
#if NET6_0_OR_GREATER
, IAsyncDisposable
#endif
where TRootDataSource : class, IRootDataSource, IDataSource, IHasExtensionCache
where TConnection : DbConnection
where TTransaction : DbTransaction
Expand Down Expand Up @@ -197,6 +200,36 @@ public void Dispose(bool disposing)
}
}

#if NET6_0_OR_GREATER
/// <summary>
/// Closes the current transaction and connection. If not committed, the transaction is rolled back.
/// </summary>
[Expose]
public ValueTask DisposeAsync()
{
return DisposeAsync(true);
}

/// <summary>
/// Closes the current transaction and connection. If not committed, the transaction is rolled back.
/// </summary>
/// <param name="disposing"></param>
[Expose(Accessibility = Accessibility.Protected, Inheritance = Inheritance.Virtual)]
public async ValueTask DisposeAsync(bool disposing)
{
if (m_Disposed)
return;

if (disposing)
{
await m_Transaction.DisposeAsync().ConfigureAwait(false);
await m_Connection.DisposeAsync().ConfigureAwait(false);
DisposableContainer?.OnDispose();
m_Disposed = true;
}
}
#endif

/// <summary>
/// The extension cache is used by extensions to store data source specific information.
/// </summary>
Expand Down Expand Up @@ -261,4 +294,4 @@ public TDatabaseMetadata DatabaseMetadata
get { return (TDatabaseMetadata)m_BaseDataSource.DatabaseMetadata; }
}
}
}
}
142 changes: 142 additions & 0 deletions Tortuga.Chain/Shared/Tests/Aggregation/ComplexAggregateTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using Tests.Models;
using Tortuga.Chain.Aggregates;

namespace Tests.Aggregate;

[TestClass]
public class ComplexAggregateTests : TestBase
{
const string Filter = "EmployeeKey < 100"; //So we don't overlfow on Sum/Avg

[DataTestMethod, TablesAndViewData(DataSourceGroup.All)]
public void MinMaxAvg(string dataSourceName, DataSourceType mode, string tableName)
{
var dataSource = DataSource(dataSourceName, mode);

try
{
//PostgreSQL is case sensitive, so we need to ensure we're using the correct name.
var table = dataSource.DatabaseMetadata.GetTableOrViewFromClass<Employee>();
var columnName = table.Columns["EmployeeKey"].SqlName;

var result = dataSource.From<Employee>(Filter).AsAggregate(
new AggregateColumn(AggregateType.Min, columnName, "MinEmployeeKey"),
new AggregateColumn(AggregateType.Max, columnName, "MaxEmployeeKey"),
new AggregateColumn(AggregateType.Count, columnName, "CountEmployeeKey")
).ToRow().Execute();

Assert.IsTrue(result.ContainsKey("MinEmployeeKey"));
Assert.IsTrue(result.ContainsKey("MaxEmployeeKey"));
Assert.IsTrue(result.ContainsKey("CountEmployeeKey"));
}
finally
{
Release(dataSource);
}
}

[DataTestMethod, TablesAndViewData(DataSourceGroup.All)]
public void MinMaxAvg_WithGroup(string dataSourceName, DataSourceType mode, string tableName)
{
var dataSource = DataSource(dataSourceName, mode);
try
{
//PostgreSQL is case sensitive, so we need to ensure we're using the correct name.
var table = dataSource.DatabaseMetadata.GetTableOrViewFromClass<Employee>();
var ekColumnName = table.Columns["EmployeeKey"].SqlName;
var gColumnName = table.Columns["Gender"].SqlName;

var result = dataSource.From<Employee>(Filter).AsAggregate(
new AggregateColumn(AggregateType.Min, ekColumnName, "MinEmployeeKey"),
new AggregateColumn(AggregateType.Max, ekColumnName, "MaxEmployeeKey"),
new AggregateColumn(AggregateType.Count, ekColumnName, "CountEmployeeKey"),
new GroupByColumn(gColumnName, "Gender"),
new CustomAggregateColumn($"Max({ekColumnName}) - Min({ekColumnName})", "Range")
).ToTable().Execute();

Assert.IsTrue(result.ColumnNames.Contains("MinEmployeeKey"));
Assert.IsTrue(result.ColumnNames.Contains("MaxEmployeeKey"));
Assert.IsTrue(result.ColumnNames.Contains("CountEmployeeKey"));
Assert.IsTrue(result.ColumnNames.Contains("Gender"));
Assert.IsTrue(result.ColumnNames.Contains("Range"));
}
finally
{
Release(dataSource);
}
}

[DataTestMethod, TablesAndViewData(DataSourceGroup.All)]
public void AggregateObject(string dataSourceName, DataSourceType mode, string tableName)
{
var dataSource = DataSource(dataSourceName, mode);
try
{
//PostgreSQL is case sensitive, so we need to ensure we're using the correct name.
var table = dataSource.DatabaseMetadata.GetTableOrViewFromClass<Employee>();
var ekColumnName = table.Columns["EmployeeKey"].SqlName;
var gColumnName = table.Columns["Gender"].SqlName;

var result = dataSource.From<Employee>(Filter).AsAggregate<EmployeeReport>().ToObject().Execute();
}
finally
{
Release(dataSource);
}
}

[DataTestMethod, TablesAndViewData(DataSourceGroup.All)]
public void AggregateObject_WithGroup(string dataSourceName, DataSourceType mode, string tableName)
{
var dataSource = DataSource(dataSourceName, mode);
try
{
//PostgreSQL is case sensitive, so we need to ensure we're using the correct name.
var table = dataSource.DatabaseMetadata.GetTableOrViewFromClass<Employee>();
var ekColumnName = table.Columns["EmployeeKey"].SqlName;
var gColumnName = table.Columns["Gender"].SqlName;

var result = dataSource.From<Employee>(Filter).AsAggregate<GroupedEmployeeReport>().ToCollection().Execute();
}
finally
{
Release(dataSource);
}
}

public class GroupedEmployeeReport
{
#if POSTGRESQL
const string ekColumnName = "employeekey";
#else
const string ekColumnName = "EmployeeKey";
#endif

[AggregateColumn(AggregateType.Min, "EmployeeKey")]
public int MinEmployeeKey { get; set; }

[AggregateColumn(AggregateType.Max, "EmployeeKey")]
public int MaxEmployeeKey { get; set; }

[AggregateColumn(AggregateType.Count, "EmployeeKey")]
public int CountEmployeeKey { get; set; }

[GroupByColumn]
public string Gender { get; set; }

[CustomAggregateColumn($"Max({ekColumnName}) - Min({ekColumnName})")]
public int Range { get; set; }
}

public class EmployeeReport
{
[AggregateColumn(AggregateType.Min, "EmployeeKey")]
public int MinEmployeeKey { get; set; }

[AggregateColumn(AggregateType.Max, "EmployeeKey")]
public int MaxEmployeeKey { get; set; }

[AggregateColumn(AggregateType.Count, "EmployeeKey")]
public int CountEmployeeKey { get; set; }
}
}
Loading

0 comments on commit cd711d4

Please sign in to comment.