LINQ for paging missing episodes

This commit is contained in:
Mark McDowall 2013-05-06 19:32:43 -07:00
parent a414a4663e
commit d37c8c26c2
26 changed files with 318 additions and 57 deletions

View File

@ -72,6 +72,8 @@
<Compile Include="IDataMapper.cs" />
<Compile Include="LazyLoaded.cs" />
<Compile Include="Mapping\FluentMappings.cs" />
<Compile Include="QGen\SqliteRowCountQueryDecorator.cs" />
<Compile Include="QGen\SqlitePagingQueryDecorator.cs" />
<Compile Include="UnitOfWork.cs" />
<Compile Include="UnitOfWorkSharedContext.cs" />
<Compile Include="Mapping\ColumnMapBuilder.cs" />

View File

@ -241,7 +241,7 @@ namespace Marr.Data.QGen
string queryText = query.Generate();
_db.SqlMode = SqlModes.Text;
int count = (int)_db.ExecuteScalar(queryText);
int count = Convert.ToInt32(_db.ExecuteScalar(queryText));
_db.SqlMode = previousSqlMode;
return count;

View File

@ -56,6 +56,9 @@ namespace Marr.Data.QGen
case DB_SqlCeClient:
return new RowCountQueryDecorator(innerQuery);
case DB_SQLiteClient:
return new SqliteRowCountQueryDecorator(innerQuery);
default:
throw new NotImplementedException("Row count has not yet been implemented for this provider.");
}
@ -74,6 +77,9 @@ namespace Marr.Data.QGen
case DB_SqlCeClient:
return new PagingQueryDecorator(innerQuery, skip, take);
case DB_SQLiteClient:
return new SqlitePagingQueryDecorator(innerQuery, skip, take);
default:
throw new NotImplementedException("Paging has not yet been implemented for this provider.");
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
@ -77,13 +78,13 @@ namespace Marr.Data.QGen
internal SortBuilder<T> Order(Type declaringType, string propertyName)
{
_sortExpressions.Add(new SortColumn<T>(declaringType, propertyName, SortDirection.Asc));
_sortExpressions.Add(new SortColumn<T>(declaringType, propertyName, ListSortDirection.Ascending));
return this;
}
internal SortBuilder<T> OrderByDescending(Type declaringType, string propertyName)
{
_sortExpressions.Add(new SortColumn<T>(declaringType, propertyName, SortDirection.Desc));
_sortExpressions.Add(new SortColumn<T>(declaringType, propertyName, ListSortDirection.Descending));
return this;
}
@ -103,25 +104,37 @@ namespace Marr.Data.QGen
public virtual SortBuilder<T> OrderBy(Expression<Func<T, object>> sortExpression)
{
_sortExpressions.Add(new SortColumn<T>(sortExpression, SortDirection.Asc));
_sortExpressions.Add(new SortColumn<T>(sortExpression, ListSortDirection.Ascending));
return this;
}
public virtual SortBuilder<T> OrderBy(Expression<Func<T, object>> sortExpression, ListSortDirection sortDirection)
{
_sortExpressions.Add(new SortColumn<T>(sortExpression, sortDirection));
return this;
}
public virtual SortBuilder<T> OrderByDescending(Expression<Func<T, object>> sortExpression)
{
_sortExpressions.Add(new SortColumn<T>(sortExpression, SortDirection.Desc));
_sortExpressions.Add(new SortColumn<T>(sortExpression, ListSortDirection.Descending));
return this;
}
public virtual SortBuilder<T> ThenBy(Expression<Func<T, object>> sortExpression)
{
_sortExpressions.Add(new SortColumn<T>(sortExpression, SortDirection.Asc));
_sortExpressions.Add(new SortColumn<T>(sortExpression, ListSortDirection.Ascending));
return this;
}
public virtual SortBuilder<T> ThenBy(Expression<Func<T, object>> sortExpression, ListSortDirection sortDirection)
{
_sortExpressions.Add(new SortColumn<T>(sortExpression, sortDirection));
return this;
}
public virtual SortBuilder<T> ThenByDescending(Expression<Func<T, object>> sortExpression)
{
_sortExpressions.Add(new SortColumn<T>(sortExpression, SortDirection.Desc));
_sortExpressions.Add(new SortColumn<T>(sortExpression, ListSortDirection.Descending));
return this;
}
@ -198,7 +211,7 @@ namespace Marr.Data.QGen
string columnName = DataHelper.GetColumnName(sort.DeclaringType, sort.PropertyName, useAltName);
sb.Append(_dialect.CreateToken(string.Format("{0}.{1}", table.Alias, columnName)));
if (sort.Direction == SortDirection.Desc)
if (sort.Direction == ListSortDirection.Descending)
sb.Append(" DESC");
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
@ -9,7 +10,24 @@ namespace Marr.Data.QGen
{
public class SortColumn<T>
{
[Obsolete("Use ListSortDirection instead")]
public SortColumn(Expression<Func<T, object>> sortExpression, SortDirection direction)
{
MemberExpression me = GetMemberExpression(sortExpression.Body);
DeclaringType = me.Expression.Type;
PropertyName = me.Member.Name;
Direction = GetSortDirection(direction);
}
[Obsolete("Use ListSortDirection instead")]
public SortColumn(Type declaringType, string propertyName, SortDirection direction)
{
DeclaringType = declaringType;
PropertyName = propertyName;
Direction = GetSortDirection(direction);
}
public SortColumn(Expression<Func<T, object>> sortExpression, ListSortDirection direction)
{
MemberExpression me = GetMemberExpression(sortExpression.Body);
DeclaringType = me.Expression.Type;
@ -17,14 +35,14 @@ namespace Marr.Data.QGen
Direction = direction;
}
public SortColumn(Type declaringType, string propertyName, SortDirection direction)
public SortColumn(Type declaringType, string propertyName, ListSortDirection direction)
{
DeclaringType = declaringType;
PropertyName = propertyName;
Direction = direction;
}
public SortDirection Direction { get; private set; }
public ListSortDirection Direction { get; private set; }
public Type DeclaringType { get; private set; }
public string PropertyName { get; private set; }
@ -40,6 +58,13 @@ namespace Marr.Data.QGen
return me;
}
private ListSortDirection GetSortDirection(SortDirection direction)
{
if (direction == SortDirection.Desc) return ListSortDirection.Descending;
return ListSortDirection.Ascending;
}
}
public enum SortDirection

View File

@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Marr.Data.Mapping;
using Marr.Data.QGen.Dialects;
namespace Marr.Data.QGen
{
/// <summary>
/// Decorates the SelectQuery by wrapping it in a paging query.
/// </summary>
public class SqlitePagingQueryDecorator : IQuery
{
private SelectQuery _innerQuery;
private int _skip;
private int _take;
public SqlitePagingQueryDecorator(SelectQuery innerQuery, int skip, int take)
{
if (string.IsNullOrEmpty(innerQuery.OrderBy.ToString()))
{
throw new DataMappingException("A paged query must specify an order by clause.");
}
_innerQuery = innerQuery;
_skip = skip;
_take = take;
}
public string Generate()
{
StringBuilder sql = new StringBuilder();
_innerQuery.BuildSelectClause(sql);
_innerQuery.BuildFromClause(sql);
_innerQuery.BuildJoinClauses(sql);
_innerQuery.BuildWhereClause(sql);
_innerQuery.BuildOrderClause(sql);
sql.AppendLine(String.Format(" LIMIT {0},{1}", _skip, _take));
return sql.ToString();
}
public void BuildSelectClause(StringBuilder sql)
{
List<string> appended = new List<string>();
sql.Append("SELECT ");
int startIndex = sql.Length;
// COLUMNS
foreach (Table join in _innerQuery.Tables)
{
for (int i = 0; i < join.Columns.Count; i++)
{
var c = join.Columns[i];
if (sql.Length > startIndex && sql[sql.Length - 1] != ',')
sql.Append(",");
if (join is View)
{
string token = _innerQuery.Dialect.CreateToken(string.Concat(join.Alias, ".", _innerQuery.NameOrAltName(c.ColumnInfo)));
if (appended.Contains(token))
continue;
sql.Append(token);
appended.Add(token);
}
else
{
string token = string.Concat(join.Alias, ".", c.ColumnInfo.Name);
if (appended.Contains(token))
continue;
sql.Append(_innerQuery.Dialect.CreateToken(token));
if (_innerQuery.UseAltName && c.ColumnInfo.AltName != null && c.ColumnInfo.AltName != c.ColumnInfo.Name)
{
string altName = c.ColumnInfo.AltName;
sql.AppendFormat(" AS {0}", altName);
}
}
}
}
}
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Marr.Data.QGen
{
public class SqliteRowCountQueryDecorator : IQuery
{
private SelectQuery _innerQuery;
public SqliteRowCountQueryDecorator(SelectQuery innerQuery)
{
_innerQuery = innerQuery;
}
public string Generate()
{
StringBuilder sql = new StringBuilder();
BuildSelectCountClause(sql);
_innerQuery.BuildFromClause(sql);
_innerQuery.BuildJoinClauses(sql);
_innerQuery.BuildWhereClause(sql);
return sql.ToString();
}
private void BuildSelectCountClause(StringBuilder sql)
{
sql.AppendLine("SELECT COUNT(*)");
}
}
}

View File

@ -45,7 +45,9 @@ namespace NzbDrone.Api
Mapper.CreateMap<PagingSpec<Episode>, PagingResource<EpisodeResource>>();
//History
Mapper.CreateMap<Core.History.History, HistoryResource>();
Mapper.CreateMap<Core.History.History, HistoryResource>()
.ForMember(dest => dest.Episode, opt => opt.Ignore())
.ForMember(dest => dest.Series, opt => opt.Ignore());
Mapper.CreateMap<PagingSpec<Core.History.History>, PagingResource<HistoryResource>>();
}
}

View File

@ -69,6 +69,24 @@ namespace NzbDrone.Api.History
// Series = series,
Indexer = "nzbs.org",
Quality = new QualityModel(Quality.HDTV720p)
},
new Core.History.History
{
Id = 2,
Date = DateTime.UtcNow.AddDays(-1),
// Episode = episode,
// Series = series,
Indexer = "nzbs.org",
Quality = new QualityModel(Quality.SDTV, true)
},
new Core.History.History
{
Id = 3,
Date = DateTime.UtcNow.AddDays(-5),
// Episode = episode,
// Series = series,
Indexer = "nzbs.org",
Quality = new QualityModel(Quality.WEBDL1080p)
}
}
};

View File

@ -36,8 +36,8 @@ namespace NzbDrone.Api.Missing
if (page == 0) page = 1;
var sortKey = PrimitiveExtensions.ToNullSafeString(Request.Query.SortKey)
.Equals("SeriesTitle", StringComparison.InvariantCultureIgnoreCase)
? "SeriesTitle"
.Equals("Series.Title", StringComparison.InvariantCultureIgnoreCase)
? "Series.Title"
: "AirDate";
var sortDirection = PrimitiveExtensions.ToNullSafeString(Request.Query.SortDir)

View File

@ -0,0 +1,25 @@
using System.ComponentModel;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.Datastore.PagingSpecExtenstionsTests
{
public class OrderByClauseFixture
{
[Test]
public void Test()
{
var pagingSpec = new PagingSpec<Episode>
{
Page = 1,
PageSize = 10,
SortDirection = ListSortDirection.Ascending,
SortKey = "AirDate"
};
pagingSpec.OrderByClause().Should().NotBeNullOrEmpty();
}
}
}

View File

@ -120,6 +120,7 @@
<Compile Include="Datastore\DatabaseRelationshipFixture.cs" />
<Compile Include="Datastore\MappingExtentionFixture.cs" />
<Compile Include="Datastore\ObjectDatabaseFixture.cs" />
<Compile Include="Datastore\PagingSpecExtenstionsTests\OrderByClauseFixture.cs" />
<Compile Include="Download\DownloadClientTests\BlackholeProviderFixture.cs" />
<Compile Include="Download\DownloadClientTests\NzbgetProviderTests\DownloadNzbFixture.cs" />
<Compile Include="Download\DownloadClientTests\NzbgetProviderTests\QueueFixture.cs" />

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
namespace NzbDrone.Core.Datastore
{
public static class PagingSpecExtensions
{
public static Expression<Func<TModel, object>> OrderByClause<TModel>(this PagingSpec<TModel> pagingSpec)
{
return CreateExpression<TModel>(pagingSpec.SortKey);
}
public static int PagingOffset<TModel>(this PagingSpec<TModel> pagingSpec)
{
return (pagingSpec.Page - 1)*pagingSpec.PageSize;
}
private static Expression<Func<TModel, object>> CreateExpression<TModel>(string propertyName)
{
Type type = typeof(TModel);
ParameterExpression parameterExpression = Expression.Parameter(type, "x");
Expression expressionBody = parameterExpression;
var splitPropertyName = propertyName.Split('.').ToList();
foreach (var property in splitPropertyName)
{
expressionBody = Expression.Property(expressionBody, property);
}
expressionBody = Expression.Convert(expressionBody, typeof(object));
return Expression.Lambda<Func<TModel, object>>(expressionBody, parameterExpression);
}
}
}

View File

@ -67,10 +67,6 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<SeriesStatistics>().MapResultSet();
}
private static void RegisterMappers()
{
RegisterEmbeddedConverter();

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using NzbDrone.Common.Messaging;
@ -39,5 +40,7 @@ namespace NzbDrone.Core.History
return null;
}
//public List<History> GetPagedHistory()
}
}

View File

@ -211,6 +211,7 @@
<Compile Include="Datastore\ModelBase.cs" />
<Compile Include="Datastore\BasicRepository.cs" />
<Compile Include="Datastore\PagingSpec.cs" />
<Compile Include="Datastore\PagingSpecExtensions.cs" />
<Compile Include="Datastore\RelationshipExtensions.cs" />
<Compile Include="Datastore\TableMapping.cs" />
<Compile Include="DecisionEngine\DownloadDecision.cs" />

View File

@ -2,7 +2,9 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using FluentMigrator.Runner;
using Marr.Data;
using Marr.Data.QGen;
using NzbDrone.Common.Messaging;
@ -65,40 +67,25 @@ namespace NzbDrone.Core.Tv
public PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec, bool includeSpecials)
{
//TODO: Join in the series title so we can do sorting on it
if (!pagingSpec.SortKey.Equals("SeriesTitle", StringComparison.InvariantCultureIgnoreCase) &&
!pagingSpec.SortKey.Equals("AirDate", StringComparison.InvariantCultureIgnoreCase))
{
throw new ArgumentException("Invalid SortKey: " + pagingSpec.SortKey, "pagingSpec");
}
if (includeSpecials)
{
throw new NotImplementedException("Including specials is not available");
}
var orderSql = String.Format("{0} {1}", pagingSpec.SortKey,
pagingSpec.SortDirection == ListSortDirection.Ascending ? "ASC" : "DESC");
var limitSql = String.Format("{0},{1}", (pagingSpec.Page - 1) * pagingSpec.PageSize, pagingSpec.PageSize);
//This causes an issue if done within the LINQ Query
var currentTime = DateTime.UtcNow;
_dataMapper.AddParameter("currentTime", currentTime);
var sql = String.Format(@"SELECT Episodes.*, Series.Title as SeriesTitle
FROM Episodes
INNER JOIN Series
ON Episodes.SeriesId = Series.Id
WHERE EpisodeFileId = 0
AND SeasonNumber > 0
AND AirDate <= @currentTime
ORDER BY {0}
LIMIT {1}",
orderSql, limitSql
);
pagingSpec.Records = Query.Join<Episode, Series>(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id)
.Where(e => e.EpisodeFileId == 0)
.AndWhere(e => e.SeasonNumber > 0)
.AndWhere(e => e.AirDate <= currentTime)
.OrderBy(pagingSpec.OrderByClause(), pagingSpec.SortDirection)
.Skip(pagingSpec.PagingOffset())
.Take(pagingSpec.PageSize)
.ToList();
pagingSpec.Records = _dataMapper.Query<Episode>(sql);
pagingSpec.TotalRecords = Query.Count(e => e.EpisodeFileId == 0 && e.SeasonNumber > 0 && e.AirDate <= currentTime);
//TODO: Use the same query for count and records
pagingSpec.TotalRecords = Query.Where(e => e.EpisodeFileId == 0 && e.SeasonNumber > 0 && e.AirDate <= currentTime).GetRowCount();
return pagingSpec;
}

View File

@ -1,8 +1,7 @@
<SolutionConfiguration>
<FileVersion>1</FileVersion>
<AutoEnableOnStartup>True</AutoEnableOnStartup>
<AutoEnableOnStartup>False</AutoEnableOnStartup>
<AllowParallelTestExecution>true</AllowParallelTestExecution>
<AllowTestsToRunInParallelWithThemselves>true</AllowTestsToRunInParallelWithThemselves>
<FrameworkUtilisationTypeForNUnit>UseDynamicAnalysis</FrameworkUtilisationTypeForNUnit>
<FrameworkUtilisationTypeForGallio>Disabled</FrameworkUtilisationTypeForGallio>
<FrameworkUtilisationTypeForMSpec>Disabled</FrameworkUtilisationTypeForMSpec>

View File

@ -1,6 +1,6 @@
"use strict";
define(['app', 'History/Model'], function () {
NzbDrone.Missing.Collection = Backbone.PageableCollection.extend({
NzbDrone.History.Collection = Backbone.PageableCollection.extend({
url : NzbDrone.Constants.ApiRoot + '/history',
model : NzbDrone.History.Model,

View File

@ -1 +1,2 @@
<i class="icon-search x-search" title="Search"></i>
<i class="icon-remove x-remove" title="Remove"></i>
<i class="icon-repeat x-redownload" title="Re-Download"></i>

View File

@ -1,2 +0,0 @@

{{seasonNumber}}x{{paddedEpisodeNumber}}

View File

@ -18,11 +18,18 @@ define([
showTable: function () {
var columns = [
{
name : 'indexer',
label : '',
editable : false,
cell : Backgrid.TemplateBackedCell.extend({ template: 'History/IndexerTemplate' }),
headerCell: 'nzbDrone'
},
{
name : 'seriesTitle',
label : 'Series Title',
editable : false,
cell : Backgrid.TemplateBackedCell.extend({ template: 'History/SeriesTitleTemplate' }),
cell : Backgrid.TemplateBackedCell.extend({ template: 'Missing/SeriesTitleTemplate' }),
headerCell: 'nzbDrone'
},
{
@ -30,11 +37,11 @@ define([
label : 'Episode',
editable : false,
sortable : false,
cell : Backgrid.TemplateBackedCell.extend({ template: 'History/EpisodeColumnTemplate' }),
cell : Backgrid.TemplateBackedCell.extend({ template: 'Missing/EpisodeColumnTemplate' }),
headerCell: 'nzbDrone'
},
{
name : 'title',
name : 'episode.title',
label : 'Episode Title',
editable : false,
sortable : false,
@ -42,8 +49,15 @@ define([
headerCell: 'nzbDrone'
},
{
name : 'airDate',
label : 'Air Date',
name : 'quality',
label : 'Quality',
editable : false,
cell : Backgrid.TemplateBackedCell.extend({ template: 'History/QualityTemplate' }),
headerCell: 'nzbDrone'
},
{
name : 'date',
label : 'Grabbed',
editable : false,
cell : 'airDate',
headerCell: 'nzbDrone'

View File

@ -0,0 +1 @@
<img src="favicon.ico" alt="{{indexer}}"/>

View File

@ -0,0 +1,5 @@
{{quality.quality.name}}
{{#if quality.proper}}
[PROPER]
{{/if}}

View File

@ -1 +0,0 @@
<a href="series/details/{{series.titleSlug}}">{{series.title}}</a>

View File

@ -20,7 +20,7 @@ define([
var columns = [
{
name : 'seriesTitle',
name : 'series.Title',
label : 'Series Title',
editable : false,
cell : Backgrid.TemplateBackedCell.extend({ template: 'Missing/SeriesTitleTemplate' }),