Skip to content

Commit

Permalink
Even more good stuff
Browse files Browse the repository at this point in the history
Major internal cleanup and refactoring;
Better support for tables with identity/primary keys other than long and
int;
More tests.
  • Loading branch information
Nima Ara committed Mar 15, 2017
1 parent 68563b1 commit d439406
Show file tree
Hide file tree
Showing 33 changed files with 963 additions and 326 deletions.
82 changes: 68 additions & 14 deletions Easy.Storage.Common/Dialect.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,80 @@
namespace Easy.Storage.Common
{
using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// Represents the kind of the <c>SQL</c> database.
/// An abstraction for specifying the set of commands and languages
/// used by the back-end storage provider which is then utilized by this library.
/// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum Dialect
public abstract class Dialect
{
/// <summary>
/// Generic <c>SQL</c> dialect.
/// </summary>
Generic = 0,
protected Dialect(DialectType type)
{
Type = type;
}

/// <summary>
/// <c>SQLite</c> specific dialect.
/// Gets the type of the dialect.
/// </summary>
SQLite,
public DialectType Type { get; }

/// <summary>
/// <c>SQLServer</c> specific dialect.
/// </summary>
SQLServer
internal virtual string GetSelectQuery(Table table)
{
var propNames = table.PropertyToColumns.Keys.Select(p => p.Name).ToArray();
var colNames = table.PropertyToColumns.Values.ToArray();
var colsAsPropNameAlias = string.Join(Formatter.ColumnSeparator, colNames.Zip(propNames, (col, prop) => $"{table.Name}.{col} AS '{prop}'"));
return $"SELECT{Formatter.NewLine}{Formatter.Spacer}{colsAsPropNameAlias}{Formatter.NewLine}FROM {table.Name}{Formatter.NewLine}WHERE{Formatter.NewLine}{Formatter.Spacer}1 = 1;";
}

internal virtual string GetDeleteQuery(Table table) => $"DELETE FROM {table.Name}{Formatter.NewLine}WHERE{Formatter.NewLine}{Formatter.Spacer}1 = 1;";

internal virtual string GetInsertQuery(Table table, bool includeIdentity)
{
var columnsAndProps = GetColumnsAndProperties(table, includeIdentity);
var insertSeg = $"INSERT INTO {table.Name}{Formatter.NewLine}({Formatter.NewLine}{Formatter.Spacer}{columnsAndProps.Key}{Formatter.NewLine})";
var valuesSeg = $"{Formatter.NewLine}VALUES{Formatter.NewLine}({Formatter.NewLine}{Formatter.Spacer}{columnsAndProps.Value}{Formatter.NewLine});";
return insertSeg + valuesSeg;
}

internal virtual string GetUpdateQuery(Table table, bool includeIdentity)
{
if (includeIdentity)
{
var propNames = table.PropertyToColumns.Keys.Select(p => p.Name).ToArray();
var colNames = table.PropertyToColumns.Values.ToArray();
var allColsEqualProp = string.Join(Formatter.ColumnSeparator, colNames.Zip(propNames, (col, propName) => $"{col} = @{propName}"));
return $"UPDATE {table.Name} SET{Formatter.NewLine}{Formatter.Spacer}{allColsEqualProp}{Formatter.NewLine}WHERE{Formatter.NewLine}{Formatter.Spacer}1 = 1;";
}

var propToColsMinusIdentity = table.PropertyToColumns.Where(p => p.Key != table.IdentityColumn).ToArray();
var colNamesMinusIdentity = propToColsMinusIdentity.Select(kv => kv.Value).ToArray();
var propNamesMinusIdentity = propToColsMinusIdentity.Select(kv => kv.Key.Name).ToArray();
var colEqualPropMinusIdentity = string.Join(Formatter.ColumnSeparator, colNamesMinusIdentity.Zip(propNamesMinusIdentity, (col, propName) => $"{col} = @{propName}"));
return $"UPDATE {table.Name} SET{Formatter.NewLine}{Formatter.Spacer}{colEqualPropMinusIdentity}{Formatter.NewLine}WHERE{Formatter.NewLine}{Formatter.Spacer}{table.PropertyToColumns[table.IdentityColumn]} = @{table.IdentityColumn.Name};";
}

protected KeyValuePair<string, string> GetColumnsAndProperties(Table table, bool includeIdentity)
{
var allColNames = table.PropertyToColumns.Select(kv => kv.Value).ToArray();
var allPropNames = table.PropertyToColumns.Select(kv => kv.Key.Name).ToArray();

string columns, properties;
if (includeIdentity)
{
columns = string.Join(Formatter.ColumnSeparator, allColNames);
properties = string.Join(Formatter.ColumnSeparator, allPropNames.Select(x => "@" + x));
} else
{
var propToColsMinusIdentity = table.PropertyToColumns.Where(p => p.Key != table.IdentityColumn).ToArray();
var colNamesMinusIdentity = propToColsMinusIdentity.Select(kv => kv.Value).ToArray();
var propNamesMinusIdentity = propToColsMinusIdentity.Select(kv => kv.Key.Name).ToArray();

columns = string.Join(Formatter.ColumnSeparator, colNamesMinusIdentity);
properties = string.Join(Formatter.ColumnSeparator, propNamesMinusIdentity.Select(x => "@" + x));
}

return new KeyValuePair<string, string>(columns, properties);
}
}
}
26 changes: 26 additions & 0 deletions Easy.Storage.Common/DialectType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Easy.Storage.Common
{
using System.Diagnostics.CodeAnalysis;

/// <summary>
/// Represents the type of the dialect used by the storage.
/// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum DialectType
{
/// <summary>
/// Generic <c>SQL</c>.
/// </summary>
Generic = 0,

/// <summary>
/// <c>SQLite</c>.
/// </summary>
SQLite,

/// <summary>
/// <c>SQLServer</c>.
/// </summary>
SQLServer
}
}
4 changes: 3 additions & 1 deletion Easy.Storage.Common/Easy.Storage.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@
<Compile Include="Attributes\AliasAttribute.cs" />
<Compile Include="Attributes\IgnoreAttribute.cs" />
<Compile Include="Attributes\IdentityAttribute.cs" />
<Compile Include="Dialect.cs" />
<Compile Include="DialectType.cs" />
<Compile Include="Extensions\DbConnectionExtensions.cs" />
<Compile Include="Extensions\HelperExtensions.cs" />
<Compile Include="Extensions\Reader.cs" />
<Compile Include="Extensions\StringExtensions.cs" />
<Compile Include="Filter\FilteredQuery.cs" />
<Compile Include="Filter\Operator.cs" />
<Compile Include="Dialect.cs" />
<Compile Include="GenericSQLDialect.cs" />
<Compile Include="TypeHandlers\DateTimeOffsetHandler.cs" />
<Compile Include="Extensions\EnumExtensions.cs" />
<Compile Include="TypeHandlers\GuidHandler.cs" />
Expand Down
2 changes: 1 addition & 1 deletion Easy.Storage.Common/Filter/FilteredQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ private FilteredQuery(Table table)

internal IDictionary<string, object> Parameters { get; }

internal static FilteredQuery Make<T>() => new FilteredQuery(Table.MakeOrGet<T>(Dialect.Generic));
internal static FilteredQuery Make<T>() => new FilteredQuery(Table.MakeOrGet<T>(GenericSQLDialect.Instance));

/// <summary>
/// Compiles and gets the <c>SQL</c> of the <see cref="FilteredQuery"/>.
Expand Down
16 changes: 16 additions & 0 deletions Easy.Storage.Common/GenericSQLDialect.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Easy.Storage.Common
{
/// <summary>
/// Represents a generic <c>SQL</c> dialect.
/// </summary>
public sealed class GenericSQLDialect : Dialect
{
static GenericSQLDialect() { }
private GenericSQLDialect() : base(DialectType.Generic) { }

/// <summary>
/// Gets a single instance of the <see cref="GenericSQLDialect"/>.
/// </summary>
public static GenericSQLDialect Instance { get; } = new GenericSQLDialect();
}
}
2 changes: 1 addition & 1 deletion Easy.Storage.Common/IRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public interface IRepository<T>
/// <param name="modelHasIdentityColumn">The flag indicating whether the table has an identity column.</param>
/// <param name="transaction">The transaction</param>
/// <returns>The inserted id of the <paramref name="item"/>.</returns>
Task<long> Insert(T item, bool modelHasIdentityColumn = true, IDbTransaction transaction = null);
Task<dynamic> Insert(T item, bool modelHasIdentityColumn = true, IDbTransaction transaction = null);

/// <summary>
/// Inserts the given <paramref name="items"/> to the storage.
Expand Down
4 changes: 2 additions & 2 deletions Easy.Storage.Common/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

[assembly: AssemblyTitle("Easy.Storage.Common")]

[assembly: AssemblyVersion("0.7.2.0")]
[assembly: AssemblyFileVersion("0.7.2.0")]
[assembly: AssemblyVersion("0.7.3.0")]
[assembly: AssemblyFileVersion("0.7.3.0")]

[assembly: AssemblyCopyright("Copyright © 2017")]

Expand Down
8 changes: 4 additions & 4 deletions Easy.Storage.Common/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,11 @@ public Task<IEnumerable<T>> GetWhere(Filter<T> filter, IDbTransaction transactio
/// <param name="modelHasIdentityColumn">The flag indicating whether the table has an identity column.</param>
/// <param name="transaction">The transaction</param>
/// <returns>The inserted id of the <paramref name="item"/>.</returns>
public async Task<long> Insert(T item, bool modelHasIdentityColumn = true, IDbTransaction transaction = null)
public async Task<object> Insert(T item, bool modelHasIdentityColumn = true, IDbTransaction transaction = null)
{
var insertSql = modelHasIdentityColumn ? Table.InsertIdentity : Table.InsertAll;
return (await _connection.QueryAsync<long>(insertSql, item, transaction: transaction, buffered: true)
.ConfigureAwait(false)).First();
return (await _connection.QueryAsync<dynamic>(insertSql, item, transaction: transaction, buffered: true)
.ConfigureAwait(false)).First().Id;
}

/// <summary>
Expand All @@ -144,7 +144,7 @@ public Task<int> Insert(IEnumerable<T> items, bool modelHasIdentityColumn = true
/// <returns>Number of rows affected</returns>
public Task<int> Update(T item, IDbTransaction transaction = null)
{
return _connection.ExecuteAsync(Table.UpdateDefault, item, transaction: transaction);
return _connection.ExecuteAsync(Table.UpdateIdentity, item, transaction: transaction);
}

/// <summary>
Expand Down
78 changes: 14 additions & 64 deletions Easy.Storage.Common/Table.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,66 +18,35 @@ public sealed class Table

internal static Table MakeOrGet<TItem>(Dialect dialect)
{
Ensure.NotNull(dialect, nameof(dialect));
var key = new TableKey(typeof(TItem), dialect);
return Cache.GetOrAdd(key, theKey => new Table(theKey));
}

internal readonly Dictionary<PropertyInfo, string> PropertyToColumns;
internal readonly Dictionary<string, string> PropertyNamesToColumns;
internal readonly PropertyInfo IdentityColumn;
// ReSharper disable once InconsistentNaming
private const string SQLServerInsertedRowDeclarationClause = "DECLARE @InsertedRows AS TABLE (Id BIGINT);";
// ReSharper disable once InconsistentNaming
private const string SQLServerSelectInsertedRowClause = "SELECT Id FROM @InsertedRows;";


private Table(TableKey key)
{
Dialect = key.Dialect;
Name = GetModelName(key.Type).GetAsEscapedSQLName();
var props = key.Type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

var props = key.Type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
IdentityColumn = GetIdentityColumn(props);
PropertyToColumns = GetPropertiesToColumnsMappings(props);
PropertyNamesToColumns = PropertyToColumns.ToDictionary(kv => kv.Key.Name, kv => kv.Value);

var propNames = PropertyToColumns.Keys.Select(p => p.Name).ToArray();
var colNames = PropertyToColumns.Values.ToArray();

var allColNames = PropertyToColumns.Select(kv => kv.Value).ToArray();
var allPropNames = PropertyToColumns.Select(kv => kv.Key.Name).ToArray();

var columns = string.Join(Formatter.ColumnSeparator, allColNames);
var properties = string.Join(Formatter.ColumnSeparator, allPropNames.Select(x => "@" + x));

var insertSeg = $"INSERT INTO {Name}{Formatter.NewLine}({Formatter.NewLine}{Formatter.Spacer}{columns}{Formatter.NewLine})";
var valuesSeg = $"{Formatter.NewLine}VALUES{Formatter.NewLine}({Formatter.NewLine}{Formatter.Spacer}{properties}{Formatter.NewLine});";
var insertAll = GetInsertQueries(Dialect, insertSeg, valuesSeg);

var propToColsMinusIdentity = PropertyToColumns.Where(p => p.Key != IdentityColumn).ToArray();
var colNamesMinusIdentity = propToColsMinusIdentity.Select(kv => kv.Value).ToArray();
var propNamesMinusIdentity = propToColsMinusIdentity.Select(kv => kv.Key.Name).ToArray();

var columnsMinusIdentity = string.Join(Formatter.ColumnSeparator, colNamesMinusIdentity);
var propertiesMinusIdentity = string.Join(Formatter.ColumnSeparator, propNamesMinusIdentity.Select(x => "@" + x));

insertSeg = $"INSERT INTO {Name}{Formatter.NewLine}({Formatter.NewLine}{Formatter.Spacer}{columnsMinusIdentity}{Formatter.NewLine})";
valuesSeg = $"{Formatter.NewLine}VALUES{Formatter.NewLine}({Formatter.NewLine}{Formatter.Spacer}{propertiesMinusIdentity}{Formatter.NewLine});";
var insertIdentity = GetInsertQueries(Dialect, insertSeg, valuesSeg);

var colsAsPropNameAlias = string.Join(Formatter.ColumnSeparator, colNames.Zip(propNames, (col, prop) => $"{Name}.{col} AS '{prop}'"));
var allColsEqualProp = string.Join(Formatter.ColumnSeparator, colNames.Zip(propNames, (col, propName) => $"{col} = @{propName}"));
var colEqualPropMinusIdentity = string.Join(Formatter.ColumnSeparator, colNamesMinusIdentity.Zip(propNamesMinusIdentity, (col, propName) => $"{col} = @{propName}"));

Select = $"SELECT{Formatter.NewLine}{Formatter.Spacer}{colsAsPropNameAlias}{Formatter.NewLine}FROM {Name}{Formatter.NewLine}WHERE{Formatter.NewLine}{Formatter.Spacer}1 = 1;";
UpdateAll = $"UPDATE {Name} SET{Formatter.NewLine}{Formatter.Spacer}{allColsEqualProp}{Formatter.NewLine}WHERE{Formatter.NewLine}{Formatter.Spacer}1 = 1;";
UpdateDefault = $"UPDATE {Name} SET{Formatter.NewLine}{Formatter.Spacer}{colEqualPropMinusIdentity}{Formatter.NewLine}WHERE{Formatter.NewLine}{Formatter.Spacer}{PropertyToColumns[IdentityColumn]} = @{IdentityColumn.Name};";
Delete = $"DELETE FROM {Name}{Formatter.NewLine}WHERE{Formatter.NewLine}{Formatter.Spacer}1 = 1;";
InsertIdentity = insertIdentity;
InsertAll = insertAll;
Select = Dialect.GetSelectQuery(this);
Delete = Dialect.GetDeleteQuery(this);
UpdateAll = Dialect.GetUpdateQuery(this, true);
UpdateIdentity = Dialect.GetUpdateQuery(this, false);
InsertAll = Dialect.GetInsertQuery(this, true);
InsertIdentity = Dialect.GetInsertQuery(this, false);
}

/// <summary>
/// Gets the type of the <c>SQL</c> database.
/// Gets the <see cref="Dialect"/> used to generate this instance.
/// </summary>
public Dialect Dialect { get; }

Expand All @@ -94,7 +63,7 @@ private Table(TableKey key)
/// <summary>
/// Gets the default <c>UPDATE</c> query to update the model based on the model's Ids.
/// </summary>
public string UpdateDefault { get; }
public string UpdateIdentity { get; }

/// <summary>
/// Gets the default <c>UPDATE</c> query to update the model based on any of the model's columns.
Expand All @@ -118,7 +87,8 @@ private Table(TableKey key)

private static PropertyInfo GetIdentityColumn(PropertyInfo[] props)
{
var possibleIdentityColumns = props.Where(p => p.CustomAttributes.Any(at => at.AttributeType == typeof(IdentityAttribute)))
var possibleIdentityColumns = props
.Where(p => p.CustomAttributes.Any(at => at.AttributeType == typeof(IdentityAttribute)))
.ToArray();

Ensure.That<InvalidOperationException>(possibleIdentityColumns.Length <= 1,
Expand All @@ -127,33 +97,13 @@ private static PropertyInfo GetIdentityColumn(PropertyInfo[] props)
// A marked Identity property has precedence over default Id property
if (possibleIdentityColumns.Length == 1) { return possibleIdentityColumns[0]; }

var defaultIdProp = props.SingleOrDefault(p => p.Name == "Id" &&
(p.PropertyType == typeof(int) || p.PropertyType == typeof(long)));
var defaultIdProp = props.SingleOrDefault(p => p.Name == "Id");

if (defaultIdProp != null) { return defaultIdProp; }

throw new InvalidOperationException("The model does not have a default 'Id' property specified or any of its members marked as Identity.");
}

private string GetInsertQueries(Dialect dialect, string insertSegment, string valuesSegment)
{
switch (dialect)
{
case Dialect.SQLite:
return $"{insertSegment}{valuesSegment}{Formatter.NewLine}SELECT last_insert_rowid();";

case Dialect.SQLServer:
var idColumnName = PropertyToColumns[IdentityColumn];
var outputClause = $"OUTPUT Inserted.{idColumnName} INTO @InsertedRows";
return $"{SQLServerInsertedRowDeclarationClause}{Formatter.NewLine}{insertSegment} {outputClause}{valuesSegment}{Formatter.NewLine}{SQLServerSelectInsertedRowClause}";

case Dialect.Generic:
return $"{insertSegment}{valuesSegment}";
}

throw new ArgumentOutOfRangeException(nameof(dialect), dialect, null);
}

private static string GetModelName(Type type)
{
var aliasAttr = type
Expand Down
Loading

0 comments on commit d439406

Please sign in to comment.