diff --git a/src/Umbraco.Core/ExpressionHelper.cs b/src/Umbraco.Core/ExpressionHelper.cs index d3c9baf1b4..e03cff4001 100644 --- a/src/Umbraco.Core/ExpressionHelper.cs +++ b/src/Umbraco.Core/ExpressionHelper.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; +using Umbraco.Core.Persistence; namespace Umbraco.Core { @@ -82,32 +83,39 @@ namespace Umbraco.Core }); } - public static MemberInfo FindProperty(LambdaExpression lambdaExpression) + public static (MemberInfo, string) FindProperty(LambdaExpression lambda) { - Expression expressionToCheck = lambdaExpression; + void Throw() + { + throw new ArgumentException($"Expression '{lambda}' must resolve to top-level member and not any child object's properties. Use a custom resolver on the child type or the AfterMap option instead.", nameof(lambda)); + } + Expression expr = lambda; var loop = true; - + string alias = null; while (loop) { - switch (expressionToCheck.NodeType) + switch (expr.NodeType) { case ExpressionType.Convert: - expressionToCheck = ((UnaryExpression) expressionToCheck).Operand; + expr = ((UnaryExpression) expr).Operand; break; case ExpressionType.Lambda: - expressionToCheck = ((LambdaExpression) expressionToCheck).Body; + expr = ((LambdaExpression) expr).Body; + break; + case ExpressionType.Call: + var callExpr = (MethodCallExpression) expr; + var method = callExpr.Method; + if (method.DeclaringType != typeof(NPocoSqlExtensions.Statics) || method.Name != "Alias" || !(callExpr.Arguments[1] is ConstantExpression aliasExpr)) + Throw(); + expr = callExpr.Arguments[0]; + alias = aliasExpr.Value.ToString(); break; case ExpressionType.MemberAccess: - var memberExpression = (MemberExpression) expressionToCheck; - - if (memberExpression.Expression.NodeType != ExpressionType.Parameter && - memberExpression.Expression.NodeType != ExpressionType.Convert) - { - throw new ArgumentException($"Expression '{lambdaExpression}' must resolve to top-level member and not any child object's properties. Use a custom resolver on the child type or the AfterMap option instead.", "lambdaExpression"); - } - - return memberExpression.Member; + var memberExpr = (MemberExpression) expr; + if (memberExpr.Expression.NodeType != ExpressionType.Parameter && memberExpr.Expression.NodeType != ExpressionType.Convert) + Throw(); + return (memberExpr.Member, alias); default: loop = false; break; diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 8297c0f3ad..e24ae133df 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -265,7 +265,7 @@ namespace Umbraco.Core.Models _publishedState = PublishedState.Publishing; } - internal virtual void RollbackValues(IContentBase other) + internal virtual void CopyValues(IContentBase other, bool published = false) { // clear all existing properties ClearEditValues(null, null); @@ -275,15 +275,15 @@ namespace Umbraco.Core.Models foreach (var otherProperty in otherProperties) { var alias = otherProperty.PropertyType.Alias; - SetValue(alias, otherProperty.GetValue(true)); + SetValue(alias, otherProperty.GetValue(published)); } } - internal virtual void RollbackValues(IContentBase other, int? nLanguageId) + internal virtual void CopyValues(IContentBase other, int? nLanguageId, bool published = false) { if (!nLanguageId.HasValue) { - RollbackValues(other); + CopyValues(other); return; } @@ -297,15 +297,15 @@ namespace Umbraco.Core.Models foreach (var otherProperty in otherProperties) { var alias = otherProperty.PropertyType.Alias; - SetValue(alias, languageId, otherProperty.GetValue(languageId, true)); + SetValue(alias, languageId, otherProperty.GetValue(languageId, published)); } } - internal virtual void RollbackValues(IContentBase other, int? nLanguageId, string segment) + internal virtual void CopyValues(IContentBase other, int? nLanguageId, string segment, bool published = false) { if (segment == null) { - RollbackValues(other, nLanguageId); + CopyValues(other, nLanguageId, published); return; } @@ -322,7 +322,7 @@ namespace Umbraco.Core.Models foreach (var otherProperty in otherProperties) { var alias = otherProperty.PropertyType.Alias; - SetValue(alias, languageId, segment, otherProperty.GetValue(languageId, segment, true)); + SetValue(alias, languageId, segment, otherProperty.GetValue(languageId, segment, published)); } } @@ -345,7 +345,7 @@ namespace Umbraco.Core.Models property.SetValue(pvalue.LanguageId, pvalue.Segment, null); } - internal virtual void RollbackAllValues(IContentBase other) + internal virtual void CopyAllValues(IContentBase other, bool published = false) { // clear all existing properties ClearEditValues(); @@ -358,12 +358,13 @@ namespace Umbraco.Core.Models foreach (var pvalue in otherProperty.Values) { // fixme can we update SetValue to accept null lang/segment and fallback? + var value = published ? pvalue.PublishedValue : pvalue.EditedValue; if (!pvalue.LanguageId.HasValue) - SetValue(alias, pvalue.PublishedValue); + SetValue(alias, value); else if (pvalue.Segment == null) - SetValue(alias, pvalue.LanguageId.Value, pvalue.PublishedValue); + SetValue(alias, pvalue.LanguageId.Value, value); else - SetValue(alias, pvalue.LanguageId.Value, pvalue.Segment, pvalue.PublishedValue); + SetValue(alias, pvalue.LanguageId.Value, pvalue.Segment, value); } } } @@ -430,6 +431,7 @@ namespace Umbraco.Core.Models var clone = (Content)DeepClone(); clone.Key = Guid.Empty; clone.Version = Guid.NewGuid(); + clone.VersionPk = clone.PublishedVersionPk = 0; clone.ResetIdentity(); foreach (var property in clone.Properties) diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 16687162cc..0f63d669e6 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -201,8 +201,8 @@ namespace Umbraco.Core.Models { foreach (var propertyValue in property.Values) { - if (propertyValue.EditValue is string editString) - propertyValue.EditValue = editString.ToValidXmlString(); + if (propertyValue.EditedValue is string editString) + propertyValue.EditedValue = editString.ToValidXmlString(); if (propertyValue.PublishedValue is string publishedString) propertyValue.PublishedValue = publishedString.ToValidXmlString(); } diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index 063da227a7..5329bf96dc 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -43,11 +43,11 @@ namespace Umbraco.Core.Models { public int? LanguageId { get; internal set; } public string Segment { get; internal set; } - public object EditValue { get; internal set; } + public object EditedValue { get; internal set; } public object PublishedValue { get; internal set; } public PropertyValue Clone() - => new PropertyValue { LanguageId = LanguageId, Segment = Segment, PublishedValue = PublishedValue, EditValue = EditValue }; + => new PropertyValue { LanguageId = LanguageId, Segment = Segment, PublishedValue = PublishedValue, EditedValue = EditedValue }; } // ReSharper disable once ClassNeverInstantiated.Local @@ -167,8 +167,8 @@ namespace Umbraco.Core.Models private object GetPropertyValue(PropertyValue pvalue, bool published) { return _propertyType.IsPublishing - ? (published ? pvalue.PublishedValue : pvalue.EditValue) - : pvalue.EditValue; + ? (published ? pvalue.PublishedValue : pvalue.EditedValue) + : pvalue.EditedValue; } internal void PublishValues() @@ -222,8 +222,8 @@ namespace Umbraco.Core.Models if (!_propertyType.IsPublishing) throw new NotSupportedException("Property type does not support publishing."); var origValue = pvalue.PublishedValue; - pvalue.PublishedValue = ConvertSetValue(pvalue.EditValue); - DetectChanges(pvalue.EditValue, origValue, Ps.Value.ValuesSelector, Ps.Value.PropertyValueComparer, false); + pvalue.PublishedValue = ConvertSetValue(pvalue.EditedValue); + DetectChanges(pvalue.EditedValue, origValue, Ps.Value.ValuesSelector, Ps.Value.PropertyValueComparer, false); } /// @@ -273,10 +273,10 @@ namespace Umbraco.Core.Models private void SetPropertyValue(PropertyValue pvalue, object value, bool change) { - var origValue = pvalue.EditValue; + var origValue = pvalue.EditedValue; var setValue = ConvertSetValue(value); - pvalue.EditValue = setValue; + pvalue.EditedValue = setValue; DetectChanges(setValue, origValue, Ps.Value.ValuesSelector, Ps.Value.PropertyValueComparer, change); } @@ -322,7 +322,7 @@ namespace Umbraco.Core.Models if (published && _propertyType.IsPublishing) pvalue.PublishedValue = value; else - pvalue.EditValue = value; + pvalue.EditedValue = value; } private (PropertyValue, bool) GetPropertyValue(bool create) @@ -351,6 +351,7 @@ namespace Umbraco.Core.Models { if (!create) return (null, false); pvalue = _lvalues[languageId] = new PropertyValue(); + pvalue.LanguageId = languageId; _values.Add(pvalue); change = true; } @@ -376,6 +377,8 @@ namespace Umbraco.Core.Models { if (!create) return (null, false); pvalue = svalue[segment] = new PropertyValue(); + pvalue.LanguageId = languageId; + pvalue.Segment = segment; _values.Add(pvalue); change = true; } diff --git a/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs b/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs index 1b58273025..9aca5ab948 100644 --- a/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs @@ -20,15 +20,9 @@ namespace Umbraco.Core.Models.Rdbms [PrimaryKeyColumn] public int Id { get; set; } - //[Column("nodeId")] - //[ForeignKey(typeof(NodeDto))] - //[Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,versionId,propertyTypeId,languageId,segment,published")] - //public int NodeId { get; set; } - [Column("versionId")] [ForeignKey(typeof(ContentVersionDto))] [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,propertyTypeId,languageId,segment")] - //[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_VersionId")] public int VersionId { get; set; } [Column("propertyTypeId")] @@ -48,10 +42,6 @@ namespace Umbraco.Core.Models.Rdbms [Length(SegmentLength)] public string Segment { get; set; } - //[Column("published")] - //[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Published")] - //public bool Published { get; set; } - [Column("intValue")] [NullSetting(NullSetting = NullSettings.Null)] public int? IntegerValue { get; set; } @@ -106,6 +96,23 @@ namespace Umbraco.Core.Models.Rdbms } } + public PropertyDataDto Clone(int versionId) + { + return new PropertyDataDto + { + VersionId = versionId, + PropertyTypeId = PropertyTypeId, + LanguageId = LanguageId, + Segment = Segment, + IntegerValue = IntegerValue, + DecimalValue = DecimalValue, + DateValue = DateValue, + VarcharValue = VarcharValue, + TextValue = TextValue, + PropertyTypeDto = PropertyTypeDto + }; + } + protected bool Equals(PropertyDataDto other) { return Id == other.Id; diff --git a/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs index 46fcc9f914..9d54f7b80d 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs @@ -131,7 +131,7 @@ namespace Umbraco.Core.Persistence.Factories { Id = ((ContentBase) entity).VersionPk, TemplateId = entity.Template?.Id, - Published = entity.Published, + Published = false, // always building the current, unpublished one ContentVersionDto = BuildContentVersionDto(entity, contentDto) }; diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index e9f0248af8..5beb9f1dc6 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -105,15 +105,15 @@ namespace Umbraco.Core.Persistence.Factories propertyDataDtos.Add(BuildDto(publishedVersionId, property, propertyValue.LanguageId, propertyValue.Segment, propertyValue.PublishedValue)); // deal with edit value - if (propertyValue.EditValue != null) - propertyDataDtos.Add(BuildDto(currentVersionId, property, propertyValue.LanguageId, propertyValue.Segment, propertyValue.EditValue)); + if (propertyValue.EditedValue != null) + propertyDataDtos.Add(BuildDto(currentVersionId, property, propertyValue.LanguageId, propertyValue.Segment, propertyValue.EditedValue)); // deal with missing edit value (fix inconsistencies) else if (propertyValue.PublishedValue != null) propertyDataDtos.Add(BuildDto(currentVersionId, property, propertyValue.LanguageId, propertyValue.Segment, propertyValue.PublishedValue)); // use explicit equals here, else object comparison fails at comparing eg strings - var sameValues = propertyValue.PublishedValue == null ? propertyValue.EditValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditValue); + var sameValues = propertyValue.PublishedValue == null ? propertyValue.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); edited |= !sameValues; } } @@ -122,8 +122,8 @@ namespace Umbraco.Core.Persistence.Factories foreach (var propertyValue in property.Values) { // not publishing = only deal with edit values - if (propertyValue.EditValue != null) - propertyDataDtos.Add(BuildDto(currentVersionId, property, propertyValue.LanguageId, propertyValue.Segment, propertyValue.EditValue)); + if (propertyValue.EditedValue != null) + propertyDataDtos.Add(BuildDto(currentVersionId, property, propertyValue.LanguageId, propertyValue.Segment, propertyValue.EditedValue)); } edited = true; } diff --git a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs b/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs deleted file mode 100644 index b7d3391318..0000000000 --- a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Models; -using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Strings; - -namespace Umbraco.Core.Persistence.Factories -{ - internal class UmbracoEntityFactory - { - internal void AddAdditionalData(UmbracoEntity entity, IDictionary originalEntityProperties) - { - var entityProps = typeof(IUmbracoEntity).GetPublicProperties().Select(x => x.Name).ToArray(); - - //figure out what extra properties we have that are not on the IUmbracoEntity and add them to additional data - foreach (var k in originalEntityProperties.Keys - .Select(x => new { orig = x, title = x.ToCleanString(CleanStringType.PascalCase | CleanStringType.Ascii | CleanStringType.ConvertCase) }) - .Where(x => entityProps.InvariantContains(x.title) == false)) - { - entity.AdditionalData[k.title] = originalEntityProperties[k.orig]; - } - } - - internal UmbracoEntity BuildEntityFromDynamic(dynamic d) - { - var asDictionary = (IDictionary) d; - var entity = new UmbracoEntity(d.trashed); - - try - { - entity.DisableChangeTracking(); - - entity.CreateDate = d.createDate; - entity.CreatorId = d.nodeUser == null ? 0 : d.nodeUser; - entity.Id = d.id; - entity.Key = d.uniqueID; - entity.Level = d.level; - entity.Name = d.text; - entity.NodeObjectTypeId = d.nodeObjectType; - entity.ParentId = d.parentID; - entity.Path = d.path; - entity.SortOrder = d.sortOrder; - entity.HasChildren = d.children > 0; - entity.ContentTypeAlias = asDictionary.ContainsKey("alias") ? (d.alias ?? string.Empty) : string.Empty; - entity.ContentTypeIcon = asDictionary.ContainsKey("icon") ? (d.icon ?? string.Empty) : string.Empty; - entity.ContentTypeThumbnail = asDictionary.ContainsKey("thumbnail") ? (d.thumbnail ?? string.Empty) : string.Empty; - //entity.VersionId = asDictionary.ContainsKey("versionId") ? asDictionary["versionId"] : Guid.Empty; - - entity.Published = asDictionary.ContainsKey("published") && (bool) asDictionary["published"]; - entity.Edited = asDictionary.ContainsKey("edits") && (bool) asDictionary["edits"]; - - // assign the additional data - AddAdditionalData(entity, asDictionary); - - return entity; - } - finally - { - entity.EnableChangeTracking(); - } - } - - } -} diff --git a/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs b/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs index ed44c6400c..e2f82072b2 100644 --- a/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs @@ -46,14 +46,14 @@ namespace Umbraco.Core.Persistence.Mappers internal DtoMapModel ResolveMapping(Expression> sourceMember, Expression> destinationMember) { var source = ExpressionHelper.FindProperty(sourceMember); - var destination = (PropertyInfo)ExpressionHelper.FindProperty(destinationMember); + var destination = (PropertyInfo) ExpressionHelper.FindProperty(destinationMember).Item1; if (destination == null) { throw new InvalidOperationException("The 'destination' returned was null, cannot resolve the mapping"); } - return new DtoMapModel(typeof(TDestination), destination, source.Name); + return new DtoMapModel(typeof(TDestination), destination, source.Item1.Name); } internal virtual string GetColumnName(ISqlSyntaxProvider sqlSyntax, Type dtoType, PropertyInfo dtoProperty) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/VariantsMigration.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/VariantsMigration.cs index 6abd82cda1..31c8d7ba55 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/VariantsMigration.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionEight/VariantsMigration.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -14,6 +15,10 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight : base(context) { } + // notes + // do NOT use Rename.Column as it's borked on SQLCE - use ReplaceColumn instead + // not sure it all runs on MySql, needs to test + public override void Up() { // delete *all* keys and indexes - because of FKs @@ -22,9 +27,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight MigratePropertyData(); MigrateContent(); - MigrateContentVersion(); - MigrateDocumentVersion(); - MigrateDocument(); + MigrateVersions(); // re-create *all* keys and indexes //Create.KeysAndIndexes(); @@ -38,27 +41,13 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight if (TableExists(Constants.DatabaseSchema.Tables.PropertyData)) return; - // add column propertyData.languageId - // add column propertyData.segment - // add column propertyData.published + // add columns if (!ColumnExists(PreTables.PropertyData, "languageId")) AddColumn(PreTables.PropertyData, "languageId"); if (!ColumnExists(PreTables.PropertyData, "segment")) AddColumn(PreTables.PropertyData, "segment"); - if (!ColumnExists(PreTables.PropertyData, "published")) - AddColumn(PreTables.PropertyData, "published"); - // do NOT use Rename.Column as it's borked on SQLCE - use ReplaceColumn instead - - // rename column propertyData.contentNodeId to nodeId - if (ColumnExists(PreTables.PropertyData, "contentNodeId")) - ReplaceColumn(PreTables.PropertyData, "contentNodeId", "nodeId"); - - // rename column propertyData.dataNtext to textValue - // rename column propertyData.dataNvarchar to varcharValue - // rename column propertyData.dataDecimal to decimalValue - // rename column propertyData.dataInt to intValue - // rename column propertyData.dataDate to dateValue + // rename columns if (ColumnExists(PreTables.PropertyData, "dataNtext")) ReplaceColumn(PreTables.PropertyData, "dataNtext", "textValue"); if (ColumnExists(PreTables.PropertyData, "dataNvarchar")) @@ -70,6 +59,26 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight if (ColumnExists(PreTables.PropertyData, "dataDate")) ReplaceColumn(PreTables.PropertyData, "dataDate", "dateValue"); + // transform column versionId from guid to integer (contentVersion.id) + if (ColumnType(PreTables.PropertyData, "versionId") == "uniqueidentifier") + { + Execute.Sql($"ALTER TABLE {PreTables.PropertyData} ADD COLUMN versionId2 INT NULL;"); + Execute.Code(context => + { + // SQLCE does not support UPDATE...FROM + var temp = context.Database.Fetch($"SELECT id, versionId FROM {PreTables.ContentVersion}"); + foreach (var t in temp) + context.Database.Execute($"UPDATE {PreTables.PropertyData} SET versionId2=@v2 WHERE versionId=@v1", new { v1 = t.versionId, v2 = t.id }); + return string.Empty; + }); + Delete.Column("versionId").FromTable(PreTables.PropertyData); + ReplaceColumn(PreTables.PropertyData, "versionId2", "versionId"); + } + + // drop column + if (ColumnExists(PreTables.PropertyData, "contentNodeId")) + Delete.Column("contentNodeId").FromTable(PreTables.PropertyData); + // rename table Rename.Table(PreTables.PropertyData).To(Constants.DatabaseSchema.Tables.PropertyData); } @@ -82,19 +91,33 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight // rename columns if (ColumnExists(PreTables.Content, "contentType")) - ReplaceColumn(PreTables.Content, "contentType", "contentTypeId"); + ReplaceColumn(PreTables.Content, "contentType", "contentTypeId"); // add columns + // fixme - why? why cannot we do it with the current version? we don't need those two columns! if (!ColumnExists(PreTables.Content, "writerUserId")) - AddColumn(PreTables.Content, "writerUserId"); + { + AddColumn(PreTables.Content, "writerUserId", out var sqls); + Execute.Sql($"UPDATE {PreTables.Content} SET writerUserId=0"); + foreach (var sql in sqls) Execute.Sql(sql); + } if (!ColumnExists(PreTables.Content, "updateDate")) - AddColumn(PreTables.Content, "updateDate"); + { + AddColumn(PreTables.Content, "updateDate", out var sqls); + var getDate = Context.SqlContext.DatabaseType.IsMySql() ? "CURRENT_TIMESTAMP" : "GETDATE()"; // sqlSyntax should do it! + Execute.Sql($"UPDATE {PreTables.Content} SET updateDate=" + getDate); + foreach (var sql in sqls) Execute.Sql(sql); + } // copy data for added columns - Execute.Sql($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Content)} -SET writerUserId=doc.documentUser, updateDate=doc.updateDate, -FROM {SqlSyntax.GetQuotedTableName(PreTables.Content)} con -JOIN {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc ON con.nodeId=doc.nodeId AND doc.newest=1"); + Execute.Code(context => + { + // SQLCE does not support UPDATE...FROM + var temp = context.Database.Fetch($"SELECT nodeId, documentUser, updateDate FROM {PreTables.Document} WHERE newest=1"); + foreach (var t in temp) + context.Database.Execute($@"UPDATE {PreTables.Content} SET writerUserId=@userId, updateDate=@updateDate", new { userId = t.documentUser, updateDate = t.updateDate }); + return string.Empty; + }); // drop columns if (ColumnExists(PreTables.Content, "pk")) @@ -104,176 +127,150 @@ JOIN {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc ON con.nodeId=doc.no Rename.Table(PreTables.Content).To(Constants.DatabaseSchema.Tables.Content); } - private void MigrateContentVersion() + private void MigrateVersions() { // if the table has already been renamed, we're done if (TableExists(Constants.DatabaseSchema.Tables.ContentVersion)) return; - // add text column - if (!ColumnExists(PreTables.ContentVersion, "text")) - AddColumn(PreTables.ContentVersion, "text"); - - // populate text column - Execute.Sql($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver -SET cver.text=doc.text -FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc WHERE cver.versionId=doc.versionId"); - - // add current column - if (!ColumnExists(PreTables.ContentVersion, "current")) - AddColumn(PreTables.ContentVersion, "current"); - - // populate current column => done during MigrateDocument - - // rename contentId column - if (ColumnExists(PreTables.ContentVersion, "ContentId")) - ReplaceColumn(PreTables.Content, "ContentId", "nodeId"); - - // rename table - Rename.Table(PreTables.ContentVersion).To(Constants.DatabaseSchema.Tables.ContentVersion); - } - - private void MigrateDocumentVersion() - { // if the table already exists, we're done if (TableExists(Constants.DatabaseSchema.Tables.DocumentVersion)) return; - // create table - Create.Table(withoutKeysAndIndexes: true); - - Execute.Sql($@"INSERT INTO {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DocumentVersion)} (id, contentVersionId, templateId) -SELECT cver.Id, cver.versionId, doc.templateId -FROM {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver -JOIN {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc ON doc.versionId=cver.versionId"); - } - - private void MigrateDocument() - { // if the table has already been renamed, we're done if (TableExists(Constants.DatabaseSchema.Tables.Document)) return; - // drop some columns - Delete.Column("text").FromTable(PreTables.Document); // fixme usage - Delete.Column("templateId").FromTable(PreTables.Document); // fixme usage - Delete.Column("documentUser").FromTable(PreTables.Document); // fixme usage - Delete.Column("updateDate").FromTable(PreTables.Document); // fixme usage + // do it all at once - // update PropertyData.Published for published versions - if (Context.SqlContext.DatabaseType.IsMySql()) + // add contentVersion columns + if (!ColumnExists(PreTables.ContentVersion, "text")) + AddColumn(PreTables.ContentVersion, "text"); + if (!ColumnExists(PreTables.ContentVersion, "current")) { - // FIXME does MySql support such update syntax? - throw new NotSupportedException(); + AddColumn(PreTables.ContentVersion, "current", out var sqls); + Execute.Sql($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET {SqlSyntax.GetQuotedColumnName("current")}=0"); + foreach (var sql in sqls) Execute.Sql(sql); } - else + if (!ColumnExists(PreTables.ContentVersion, "userId")) { - Execute.Sql($@"UPDATE pdata -SET pdata.published=1 -FROM {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.PropertyData)} pdata -JOIN {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc ON doc.versionId=pdata.versionId AND doc.published=1"); + AddColumn(PreTables.ContentVersion, "userId", out var sqls); + Execute.Sql($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET userId=0"); + foreach (var sql in sqls) Execute.Sql(sql); } - // collapse draft version into published version (if any) - // ie we keep the published version and remove the draft one + // rename contentVersion contentId column + if (ColumnExists(PreTables.ContentVersion, "ContentId")) + ReplaceColumn(PreTables.ContentVersion, "ContentId", "nodeId"); + + // populate contentVersion text, current and userId columns for documents Execute.Code(context => { - // xxx1 is published, non-newest - // xxx2 is newest, non-published - // we want to move data from 2 to 1 - var versions = context.Database.Fetch(@"SELECT - doc1.versionId versionId1, doc1.newest newest1, doc1.published published1, - doc2.versionId versionId2, doc2.newest newest2, doc2.published published2 -FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc1 -JOIN {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc2 - ON doc1.nodeId=doc2.nodeId AND doc1.versionId<>doc2.versionId AND doc1.updateDate($"SELECT versionId, text, newest, documentUser FROM {PreTables.Document}"); + foreach (var t in temp) + context.Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=@current, userId=@userId WHERE versionId=@versionId", + new { text = t.text, current = t.newest, userId=t.documentUser, versionId=t.versionId }); return string.Empty; }); - // update content version - Execute.Sql($@"UPDATE {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.ContentVersion)} -SET current=doc.newest -FROM {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.ContentVersion)} ver -JOIN {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc ON ver.versionId=doc.versionId"); - - // keep only one row per document + // populate contentVersion text and current columns for non-documents, userId is default Execute.Code(context => { - var versions = context.Database.Fetch(@"SELECT - doc.nodeId, doc.versionId versionId1 + // SQLCE does not support UPDATE...FROM + var temp = context.Database.Fetch($@"SELECT cver.versionId, n.text +FROM {PreTables.ContentVersion} cver +JOIN {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.Node)} n ON cver.nodeId=n.id +WHERE cver.versionId NOT IN (SELECT versionId FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)})"); + + foreach (var t in temp) + context.Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 WHERE versionId=@versionId", + new { text = t.text, versionId=t.versionId }); + return string.Empty; + }); + + // create table + Create.Table(withoutKeysAndIndexes: true); + + // every document row becomes a document version + Execute.Sql($@"INSERT INTO {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) +SELECT cver.id, doc.templateId, doc.published +FROM {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver +JOIN {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc ON doc.versionId=cver.versionId"); + + // need to add extra rows for where published=newest + // 'cos INSERT above has inserted the 'published' document version + // and v8 always has a 'edited' document version too + Execute.Code(context => + { + var temp = context.Database.Fetch($@"SELECT doc.nodeId, doc.versionId, doc.updateDate, doc.documentUser, doc.text, doc.templateId FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc -WHERE doc.newest=1 -"); - foreach (var version in versions) +WHERE doc.newest=1 AND doc.published=1"); + foreach (var t in temp) { - context.Database.Execute($@"DELETE FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} -WHERE nodeId={version.nodeId} AND versionId<>{version.versionId}"); + context.Database.Execute($@"INSERT INTO {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} (nodeId, versionId, versionDate, userId, {SqlSyntax.GetQuotedColumnName("current")}, text) +VALUES (@nodeId, @versionId, @versionDate, @userId, 1, @text)", new { nodeId=t.nodeId, versionId= Guid.NewGuid(), versionDate=t.updateDate, userId=t.documentUser, text=t.text }); + var id = context.Database.ExecuteScalar($@"SELECT @@@@IDENTITY"); // fixme mysql + context.Database.Execute($@"INSERT INTO {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) +VALUES (@id, @templateId, 1)", new { id=id, templateId=t.templateId }); } return string.Empty; }); - // drop some columns - Delete.Column("versionId").FromTable(PreTables.Document); // fixme usage - Delete.Column("newest").FromTable(PreTables.Document); // fixme usage + // fixme these extra rows need propertydata too! - // ensure that every 'published' property data has a corresponding 'non-published' one - // but only for the current version - Execute.Sql($@"INSERT INTO {Constants.DatabaseSchema.Tables.PropertyData} (nodeId, versionId, propertyTypeId, languageId, segment, published, intValue, decimalValue, dateValue, varcharValue, textValue) -SELECT p1.nodeId, p1.versionId, p1.propertyTypeId, p1.languageId, p1.segment, 0, p1.intValue, p1.decimalValue, p1.dateValue, p1.varcharValue, p1.textValue -FROM {Constants.DatabaseSchema.Tables.PropertyData} p1 -JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON p1.versionId=cver.versionId AND cver.current=1 -WHERE NOT EXIST ( - SELECT p2.id - FROM {Constants.DatabaseSchema.Tables.PropertyData} p2 - WHERE - p1.nodeId=p2.nodeId AND p1.versionId=p2.versionId - AND p1.propertyTypeId=p2.propertyTypeId - AND p1.lang=p2.lang AND p1.segment=p2.segment - AND p2.published=0 -)"); + // reduce document to 1 row per content + Execute.Sql($@"DELETE FROM {PreTables.Document} +WHERE versionId NOT IN (SELECT (versionId) FROM {PreTables.ContentVersion} WHERE {SqlSyntax.GetQuotedColumnName("current")} = 1)"); - // create some columns - if (!ColumnExists(PreTables.Document, "edits")) + // drop some document columns + Delete.Column("text").FromTable(PreTables.Document); + Delete.Column("templateId").FromTable(PreTables.Document); + Delete.Column("documentUser").FromTable(PreTables.Document); + Delete.Column("updateDate").FromTable(PreTables.Document); + Delete.Column("versionId").FromTable(PreTables.Document); + Delete.Column("newest").FromTable(PreTables.Document); + + // add and populate edited column + if (!ColumnExists(PreTables.Document, "edited")) { - AddColumn(PreTables.Document, "edits", out var notNull); - Execute.Sql($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edits=0"); - Execute.Sql(notNull); - - // set 'edits' to true whenever a 'non-published' property data is != a published one - Execute.Sql($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edits=0"); - - Execute.Sql($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edits=1 WHERE nodeId IN ( -SELECT p1.nodeId -FROM {Constants.DatabaseSchema.Tables.PropertyData} p1 -JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON p1.versionId=cver.versionId AND cver.current=1 -JOIN {Constants.DatabaseSchema.Tables.PropertyData} p2 -ON p1.nodeId=p2.nodeId AND p1.versionId=p2.versionId AND AND p1.propertyTypeId=p2.propertyTypeId AND p1.lang=p2.lang AND p1.segment=p2.segment AND p2.published=0 - AND (p1.intValue<>p2.intValue OR p1.decimalValue<>p2.decimalValue OR p1.dateValue<>p2.dateValue OR p1.varcharValue<>p2.varcharValue OR p1.textValue<>p2.textValue) -WHERE p1.published=1)"); + AddColumn(PreTables.Document, "edited", out var sqls); + Execute.Sql($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=0"); + foreach (var sql in sqls) Execute.Sql(sql); } - // rename table + // set 'edited' to true whenever a 'non-published' property data is != a published one + Execute.Code(context => + { + // cannot compare NTEXT values in TSQL + // cannot cast NTEXT to NVARCHAR(MAX) in SQLCE + // ... bah ... + var temp = context.Database.Fetch($@"SELECT n.id, +v1.intValue intValue1, v1.decimalValue decimalValue1, v1.dateValue dateValue1, v1.varcharValue varcharValue1, v1.textValue textValue1, +v2.intValue intValue2, v2.decimalValue decimalValue2, v2.dateValue dateValue2, v2.varcharValue varcharValue2, v2.textValue textValue2 +FROM {Constants.DatabaseSchema.Tables.Node} n +JOIN {PreTables.ContentVersion} cv1 ON n.id=cv1.nodeId AND cv1.{SqlSyntax.GetQuotedColumnName("current")}=1 +JOIN {Constants.DatabaseSchema.Tables.PropertyData} v1 ON cv1.id=v1.versionId +JOIN {PreTables.ContentVersion} cv2 ON n.id=cv2.nodeId +JOIN {Constants.DatabaseSchema.Tables.DocumentVersion} dv ON cv2.id=dv.id AND dv.published=1 +JOIN {Constants.DatabaseSchema.Tables.PropertyData} v2 ON cv2.id=v2.versionId +WHERE v1.propertyTypeId=v2.propertyTypeId AND v1.languageId=v2.languageId AND v1.segment=v2.segment"); + + foreach (var t in temp) + if (t.intValue1 != t.intValue2 || t.decimalValue1 != t.decimalValue2 || t.dateValue1 != t.dateValue2 || t.varcharValue1 != t.varcharValue2 || t.textValue1 != t.textValue2) + context.Database.Execute("UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=1 WHERE nodeId=@nodeIdd", new { t.id }); + + return string.Empty; + }); + + // rename tables + Rename.Table(PreTables.ContentVersion).To(Constants.DatabaseSchema.Tables.ContentVersion); Rename.Table(PreTables.Document).To(Constants.DatabaseSchema.Tables.Document); } private static class PreTables { + // ReSharper disable UnusedMember.Local public const string Lock = "umbracoLock"; public const string Log = "umbracoLog"; @@ -335,40 +332,31 @@ WHERE p1.published=1)"); public const string Task = "cmsTask"; public const string TaskType = "cmsTaskType"; + // ReSharper restore UnusedMember.Local } private void AddColumn(string tableName, string columnName) { - AddColumn(tableName, columnName, out var notNull); - if (notNull != null) Execute.Sql(notNull); + AddColumn(tableName, columnName, out var sqls); + foreach (var sql in sqls) Execute.Sql(sql); } - private void AddColumn(string tableName, string columnName, out string notNull) + private void AddColumn(string tableName, string columnName, out IEnumerable sqls) { - if (ColumnExists(tableName, columnName)) - throw new InvalidOperationException($"Column {tableName}.{columnName} already exists."); + //if (ColumnExists(tableName, columnName)) + // throw new InvalidOperationException($"Column {tableName}.{columnName} already exists."); var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); var column = table.Columns.First(x => x.Name == columnName); - var create = SqlSyntax.Format(column); - // some db cannot add a NOT NULL column, so change it into NULL - if (create.Contains("NOT NULL")) - { - notNull = string.Format(SqlSyntax.AlterColumn, SqlSyntax.GetQuotedTableName(tableName), create); - create = create.Replace("NOT NULL", "NULL"); - } - else - { - notNull = null; - } - Execute.Sql($"ALTER TABLE {SqlSyntax.GetQuotedTableName(tableName)} ADD COLUMN " + create); + var createSql = SqlSyntax.Format(column, SqlSyntax.GetQuotedTableName(tableName), out sqls); + Execute.Sql(string.Format(SqlSyntax.AddColumn, SqlSyntax.GetQuotedTableName(tableName), createSql)); } private void ReplaceColumn(string tableName, string currentName, string newName) { - AddColumn(tableName, newName, out var notNull); + AddColumn(tableName, newName, out var sqls); Execute.Sql($"UPDATE {SqlSyntax.GetQuotedTableName(tableName)} SET {SqlSyntax.GetQuotedColumnName(newName)}={SqlSyntax.GetQuotedColumnName(currentName)}"); - if (notNull != null) Execute.Sql(notNull); + foreach (var sql in sqls) Execute.Sql(sql); Delete.Column(currentName).FromTable(tableName); } @@ -384,5 +372,13 @@ WHERE p1.published=1)"); var columns = SqlSyntax.GetColumnsInSchema(Context.Database).Distinct().ToArray(); return columns.Any(x => x.TableName.InvariantEquals(tableName) && x.ColumnName.InvariantEquals(columnName)); } + + private string ColumnType(string tableName, string columnName) + { + // that's ok even on MySql + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).Distinct().ToArray(); + var column = columns.FirstOrDefault(x => x.TableName.InvariantEquals(tableName) && x.ColumnName.InvariantEquals(columnName)); + return column?.DataType; + } } } diff --git a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs index 9541306261..0c85e57e31 100644 --- a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs @@ -6,7 +6,6 @@ using System.Linq.Expressions; using System.Reflection; using System.Text; using NPoco; -using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; @@ -18,9 +17,48 @@ namespace Umbraco.Core.Persistence // when doing "sql = sql.Where(...)" actually append to, and return, the original Sql, not // a new one. - #region Alias + #region Special extensions - // would love to implement per-column alias in select, where, etc... + /// + /// Provides a mean to express aliases in SELECT Sql statements. + /// + /// + /// First register with using static Umbraco.Core.Persistence.NPocoSqlExtensions.Aliaser, + /// then use eg Sql{Foo}(x => Alias(x.Id, "id")). + /// + public static class Statics + { + /// + /// Aliases a field. + /// + /// The field to alias. + /// The alias. + public static object Alias(object field, string alias) => field; + + /// + /// Produces Sql text. + /// + /// The name of the field. + /// A function producing Sql text. + public static T SqlText(string field, Func expr) => default; + + /// + /// Produces Sql text. + /// + /// The name of the first field. + /// The name of the second field. + /// A function producing Sql text. + public static T SqlText(string field1, string field2, Func expr) => default; + + /// + /// Produces Sql text. + /// + /// The name of the first field. + /// The name of the second field. + /// The name of the third field. + /// A function producing Sql text. + public static T SqlText(string field1, string field2, string field3, Func expr) => default; + } #endregion @@ -32,6 +70,7 @@ namespace Umbraco.Core.Persistence /// The type of the Dto. /// The Sql statement. /// A predicate to transform and append to the Sql statement. + /// An optional alias for the table. /// The Sql statement. public static Sql Where(this Sql sql, Expression> predicate, string alias = null) { @@ -146,6 +185,58 @@ namespace Umbraco.Core.Persistence return sql; } + /// + /// Appends multiple OR WHERE clauses to the Sql statement. + /// + /// The Sql statement. + /// The WHERE predicates. + /// The Sql statement. + public static Sql WhereAny(this Sql sql, params Func, Sql>[] predicates) + { + var wsql = new Sql(sql.SqlContext); + + wsql.Append("("); + for (var i = 0; i < predicates.Length; i++) + { + if (i > 0) + wsql.Append(") OR ("); + var temp = new Sql(sql.SqlContext); + temp = predicates[i](temp); + wsql.Append(temp.SQL.TrimStart("WHERE "), temp.Arguments); + } + wsql.Append(")"); + + return sql.Where(wsql.SQL, wsql.Arguments); + } + + /// + /// Appends a WHERE NOT NULL clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// Expression specifying the field. + /// An optional alias for the table. + /// The Sql statement. + public static Sql WhereNotNull(this Sql sql, Expression> field, string tableAlias = null) + { + return sql.WhereNull(field, tableAlias, true); + } + + /// + /// Appends a WHERE [NOT] NULL clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// Expression specifying the field. + /// An optional alias for the table. + /// A value indicating whether to NOT NULL. + /// The Sql statement. + public static Sql WhereNull(this Sql sql, Expression> field, string tableAlias = null, bool not = false) + { + var column = sql.GetColumns(columnExpressions: new[] { field }, tableAlias: tableAlias, withAlias: false).First(); + return sql.Where("(" + column + " IS " + (not ? "NOT " : "") + "NULL)"); + } + #endregion #region From @@ -183,7 +274,7 @@ namespace Umbraco.Core.Persistence /// The Sql statement. public static Sql OrderBy(this Sql sql, Expression> field) { - return sql.OrderBy("(" + GetFieldName(field, sql.SqlContext.SqlSyntax) + ")"); // fixme - explain (...) + return sql.OrderBy("(" + GetFieldName(field, sql.SqlContext.SqlSyntax) + ")"); } /// @@ -210,7 +301,7 @@ namespace Umbraco.Core.Persistence /// The Sql statement. public static Sql OrderByDescending(this Sql sql, Expression> field) { - return sql.OrderBy("(" + GetFieldName(field, sql.SqlContext.SqlSyntax) + ") DESC"); // fixme - explain (...) + return sql.OrderBy("(" + GetFieldName(field, sql.SqlContext.SqlSyntax) + ") DESC"); } /// @@ -510,16 +601,19 @@ namespace Umbraco.Core.Persistence } /// - /// Creates a SELECT Sql statement for an aliased column. + /// Creates a SELECT Sql statement. /// /// The type of the DTO to select. /// The origin sql. - /// Expression indicating the column to select. - /// The column alias + /// A table alias. + /// Expressions indicating the columns to select. /// The Sql statement. - public static Sql SelectAs(this Sql sql, Expression> field, string alias) + /// + /// If is empty, all columns are selected. + /// + public static Sql Select(this Sql sql, string tableAlias, params Expression>[] fields) { - return sql.Select(string.Join(", ", sql.GetColumns(columnExpressions: new[] { field }, withAlias: false)) + " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias)); + return sql.Select(sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields)); } /// @@ -537,17 +631,21 @@ namespace Umbraco.Core.Persistence return sql.Append(", " + string.Join(", ", sql.GetColumns(columnExpressions: fields))); } + /// - /// Adds an aliased column to a SELECT Sql statement. + /// Adds columns to a SELECT Sql statement. /// /// The type of the DTO to select. /// The origin sql. - /// Expression indicating the column to select. - /// The column alias + /// A table alias. + /// Expressions indicating the columns to select. /// The Sql statement. - public static Sql AndSelectAs(this Sql sql, Expression> field, string alias) + /// + /// If is empty, all columns are selected. + /// + public static Sql AndSelect(this Sql sql, string tableAlias, params Expression>[] fields) { - return sql.Append(", " + string.Join(", ", sql.GetColumns(columnExpressions: new[] { field }, withAlias: false)) + " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias)); + return sql.Append(", " + string.Join(", ", sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields))); } /// @@ -632,7 +730,7 @@ namespace Umbraco.Core.Persistence /// A SqlRef statement. public SqlRef Select(Expression> field, string tableAlias, Func, SqlRef> reference = null) { - var property = field == null ? null : ExpressionHelper.FindProperty(field) as PropertyInfo; + var property = field == null ? null : ExpressionHelper.FindProperty(field).Item1 as PropertyInfo; return Select(property, tableAlias, reference); } @@ -662,7 +760,7 @@ namespace Umbraco.Core.Persistence /// public SqlRef Select(Expression>> field, string tableAlias, Func, SqlRef> reference = null) { - var property = field == null ? null : ExpressionHelper.FindProperty(field) as PropertyInfo; + var property = field == null ? null : ExpressionHelper.FindProperty(field).Item1 as PropertyInfo; return Select(property, tableAlias, reference); } @@ -796,21 +894,38 @@ namespace Umbraco.Core.Persistence var tableName = tableAlias ?? pd.TableInfo.TableName; var queryColumns = pd.QueryColumns; + Dictionary aliases = null; + if (columnExpressions != null && columnExpressions.Length > 0) { var names = columnExpressions.Select(x => { - var field = ExpressionHelper.FindProperty(x) as PropertyInfo; + (var member, var alias) = ExpressionHelper.FindProperty(x); + var field = member as PropertyInfo; var fieldName = field.GetColumnName(); + if (alias != null) + { + if (aliases == null) + aliases = new Dictionary(); + aliases[fieldName] = alias; + } return fieldName; }).ToArray(); queryColumns = queryColumns.Where(x => names.Contains(x.Key)).ToArray(); } + string GetAlias(PocoColumn column) + { + if (aliases != null && aliases.TryGetValue(column.ColumnName, out var alias)) + return alias; + + return withAlias ? (string.IsNullOrEmpty(column.ColumnAlias) ? column.MemberInfoKey : column.ColumnAlias) : null; + } + return queryColumns.Select(x => (object) GetColumn(sql.SqlContext.DatabaseType, tableName, x.Value.ColumnName, - withAlias ? (string.IsNullOrEmpty(x.Value.ColumnAlias) ? x.Value.MemberInfoKey : x.Value.ColumnAlias) : null, + GetAlias(x.Value), referenceName)).ToArray(); } @@ -844,7 +959,7 @@ namespace Umbraco.Core.Persistence private static string GetFieldName(Expression> fieldSelector, ISqlSyntaxProvider sqlSyntax) { - var field = ExpressionHelper.FindProperty(fieldSelector) as PropertyInfo; + var field = ExpressionHelper.FindProperty(fieldSelector).Item1 as PropertyInfo; var fieldName = field.GetColumnName(); var type = typeof (TDto); diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs index ba6d919718..a43e99177e 100644 --- a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs @@ -506,6 +506,9 @@ namespace Umbraco.Core.Persistence.Querying if (methodArgs[0].NodeType != ExpressionType.Constant) { + // if it's a field accessor, we could Visit(methodArgs[0]) and get [a].[b] + // but then, what if we want more, eg .StartsWith(node.Path + ',') ? => not + //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) // So we'll go get the value: var member = Expression.Convert(methodArgs[0], typeof(object)); @@ -653,6 +656,42 @@ namespace Umbraco.Core.Persistence.Querying // return string.Format("{0} As {1}", r, // GetQuotedColumnName(RemoveQuoteFromAlias(RemoveQuote(args[0].ToString())))); + case "SqlText": + if (m.Method.DeclaringType != typeof(NPocoSqlExtensions.Statics)) + goto default; + if (m.Arguments.Count == 2) + { + var n1 = Visit(m.Arguments[0]); + var f = m.Arguments[2]; + if (!(f is Expression> fl)) + throw new NotSupportedException("Expression is not a proper lambda."); + var ff = fl.Compile(); + return ff(n1); + } + else if (m.Arguments.Count == 3) + { + var n1 = Visit(m.Arguments[0]); + var n2 = Visit(m.Arguments[1]); + var f = m.Arguments[2]; + if (!(f is Expression> fl)) + throw new NotSupportedException("Expression is not a proper lambda."); + var ff = fl.Compile(); + return ff(n1, n2); + } + else if (m.Arguments.Count == 4) + { + var n1 = Visit(m.Arguments[0]); + var n2 = Visit(m.Arguments[1]); + var n3 = Visit(m.Arguments[3]); + var f = m.Arguments[3]; + if (!(f is Expression> fl)) + throw new NotSupportedException("Expression is not a proper lambda."); + var ff = fl.Compile(); + return ff(n1, n2, n3); + } + else + throw new NotSupportedException("Expression is not a proper lambda."); + default: throw new ArgumentOutOfRangeException("No logic supported for " + m.Method.Name); diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index aa8eba63b0..8df698ce0f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -1,6 +1,7 @@ using NPoco; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; @@ -122,16 +123,24 @@ namespace Umbraco.Core.Persistence.Repositories .InnerJoin().On(left => left.NodeId, right => right.NodeId) .InnerJoin().On(left => left.NodeId, right => right.NodeId) - // inner join on mandatory current version + // inner join on mandatory edited version .InnerJoin().On((left, right) => left.NodeId == right.NodeId) .InnerJoin().On((left, right) => left.Id == right.Id) // left join on optional published version .LeftJoin(nested => nested.InnerJoin("pdv").On((left, right) => left.Id == right.Id && right.Published, "pcv", "pdv"), "pcv") - .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcv"); + .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcv"); - sql.Where(x => x.NodeObjectType == NodeObjectTypeId); + sql + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + // this would ensure we don't get the published version - keep for reference + //sql + // .WhereAny( + // x => x.Where((x1, x2) => x1.Id != x2.Id, alias2: "pcv"), + // x => x.WhereNull(x1 => x1.Id, "pcv") + // ); if (current) sql.Where(x => x.Current); // always get the current version @@ -222,6 +231,7 @@ namespace Umbraco.Core.Persistence.Repositories var content = (Content) entity; content.AddingEntity(); + var publishing = content.PublishedState == PublishedState.Publishing; // ensure that the default template is assigned if (entity.Template == null) @@ -276,25 +286,29 @@ namespace Umbraco.Core.Persistence.Repositories // persist the content version dto var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; contentVersionDto.NodeId = nodeDto.NodeId; - contentVersionDto.Current = true; + contentVersionDto.Current = !publishing; Database.Insert(contentVersionDto); content.VersionPk = contentVersionDto.Id; // persist the document version dto var documentVersionDto = dto.DocumentVersionDto; documentVersionDto.Id = content.VersionPk; + if (publishing) + documentVersionDto.Published = true; Database.Insert(documentVersionDto); // and again in case we're publishing immediately - if (content.PublishedState == PublishedState.Publishing) + if (publishing) { content.PublishedVersionPk = content.VersionPk; content.Version = contentVersionDto.VersionId = Guid.NewGuid(); contentVersionDto.Id = 0; + contentVersionDto.Current = true; Database.Insert(contentVersionDto); content.VersionPk = contentVersionDto.Id; documentVersionDto.Id = content.VersionPk; + documentVersionDto.Published = false; Database.Insert(documentVersionDto); } @@ -312,7 +326,7 @@ namespace Umbraco.Core.Persistence.Repositories content.Edited = dto.Edited = edited; Database.Insert(dto); - // fixme - here or at the very end? + // trigger here, before we reset Published etc OnUowRefreshedEntity(new UnitOfWorkEntityEventArgs(UnitOfWork, entity)); // flip the entity's published property @@ -339,6 +353,18 @@ namespace Umbraco.Core.Persistence.Repositories UpdateEntityTags(entity, _tagRepository); entity.ResetDirtyProperties(); + + // troubleshooting + //if (Database.ExecuteScalar("SELECT COUNT(*) FROM uDocumentVersion JOIN uContentVersion ON uDocumentVersion.id=uContentVersion.id WHERE published=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} + //if (Database.ExecuteScalar("SELECT COUNT(*) FROM uDocumentVersion JOIN uContentVersion ON uDocumentVersion.id=uContentVersion.id WHERE [current]=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} } protected override void PersistUpdatedItem(IContent entity) @@ -357,17 +383,13 @@ namespace Umbraco.Core.Persistence.Repositories // update content.UpdatingEntity(); + var publishing = content.PublishedState == PublishedState.Publishing; // check if we need to create a new version - var publishing = content.PublishedState == PublishedState.Publishing; - if (publishing) + if (publishing && content.PublishedVersionPk > 0) { // published version is not published anymore Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == content.PublishedVersionPk)); - - // current version is now published, but not current anymore - Database.Execute(Sql().Update(u => u.Set(x => x.Published, true)).Where(x => x.Id == content.VersionPk)); - Database.Execute(Sql().Update(u => u.Set(x => x.Current, false)).Where(x => x.Id == content.VersionPk)); } // ensure unique name on the same level @@ -400,6 +422,11 @@ namespace Umbraco.Core.Persistence.Repositories // update the content & document version dtos var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; var documentVersionDto = dto.DocumentVersionDto; + if (publishing) + { + documentVersionDto.Published = true; // now published + contentVersionDto.Current = false; // no more current + } Database.Update(contentVersionDto); Database.Update(documentVersionDto); @@ -409,10 +436,12 @@ namespace Umbraco.Core.Persistence.Repositories content.PublishedVersionPk = content.VersionPk; contentVersionDto.Id = 0; // want a new id + contentVersionDto.Current = true; // current version content.Version = contentVersionDto.VersionId = Guid.NewGuid(); // for that new guid Database.Insert(contentVersionDto); content.VersionPk = documentVersionDto.Id = contentVersionDto.Id; // get the new id + documentVersionDto.Published = false; // non-published version Database.Insert(documentVersionDto); } @@ -440,7 +469,7 @@ namespace Umbraco.Core.Persistence.Repositories if (content.PublishedState == PublishedState.Publishing) UpdateEntityTags(entity, _tagRepository); - // fixme - here or at the very end? + // trigger here, before we reset Published etc OnUowRefreshedEntity(new UnitOfWorkEntityEventArgs(UnitOfWork, entity)); // flip the entity's published property @@ -463,6 +492,18 @@ namespace Umbraco.Core.Persistence.Repositories } entity.ResetDirtyProperties(); + + // troubleshooting + //if (Database.ExecuteScalar("SELECT COUNT(*) FROM uDocumentVersion JOIN uContentVersion ON uDocumentVersion.id=uContentVersion.id WHERE published=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} + //if (Database.ExecuteScalar("SELECT COUNT(*) FROM uDocumentVersion JOIN uContentVersion ON uDocumentVersion.id=uContentVersion.id WHERE [current]=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} } protected override void PersistDeletedItem(IContent entity) diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 2b4c38bcd3..3532370076 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Data; using System.Globalization; using System.Linq; -using System.Text.RegularExpressions; -using System.Threading; using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Events; @@ -13,13 +11,11 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; - using Umbraco.Core.Persistence.Factories; -using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Services; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories { @@ -457,7 +453,7 @@ AND umbracoNode.id <> @id", private string EnsureUniqueNodeName(string nodeName, int id = 0) { var template = SqlContext.Templates.Get("Umbraco.Core.DataTypeDefinitionRepository.EnsureUniqueNodeName", tsql => tsql - .SelectAs(x => x.NodeId, "id").AndSelectAs(x => x.Text, "name") + .Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name")) .From() .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType"))); diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs index 5f6dc640e3..5afb1abdee 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs @@ -1,18 +1,19 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using NPoco; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories { + // fixme - use sql templates everywhere! + /// /// Represents the EntityRepository used to query objects. /// @@ -26,141 +27,119 @@ namespace Umbraco.Core.Persistence.Repositories UnitOfWork = work; } - protected internal IScopeUnitOfWork UnitOfWork { get; } + protected IScopeUnitOfWork UnitOfWork { get; } + protected IUmbracoDatabase Database => UnitOfWork.Database; protected Sql Sql() => UnitOfWork.SqlContext.Sql(); - #region Query Methods + #region Repository - // fixme need to review that ... one - public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectTypeId, long pageIndex, int pageSize, out long totalRecords, + // get a page of entities + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, IQuery filter = null) { - var isContent = objectTypeId == Constants.ObjectTypes.Document || objectTypeId == Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectTypeId == Constants.ObjectTypes.Media; - var factory = new UmbracoEntityFactory(); + var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectType == Constants.ObjectTypes.Media; - var sqlClause = GetBaseWhere(GetBase, isContent, isMedia, sql => + var sql = GetBaseWhere(isContent, isMedia, false, x => { - if (filter != null) - { - foreach (var filterClause in filter.GetWhereClauses()) - { - sql.Where(filterClause.Item1, filterClause.Item2); - } - } - }, objectTypeId); - var translator = new SqlTranslator(sqlClause, query); - var entitySql = translator.Translate(); - var pagedSql = entitySql.Append(GetGroupBy(isContent, isMedia, false)).OrderBy("umbracoNode.id"); + if (filter == null) return; + foreach (var filterClause in filter.GetWhereClauses()) + x.Where(filterClause.Item1, filterClause.Item2); + }, objectType); - IEnumerable result; + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + sql = AddGroupBy(isContent, isMedia, sql); + sql = sql.OrderBy(x => x.NodeId); + + //IEnumerable result; + // + //if (isMedia) + //{ + // //Treat media differently for now, as an Entity it will be returned with ALL of it's properties in the AdditionalData bag! + // var pagedResult = UnitOfWork.Database.Page(pageIndex + 1, pageSize, pagedSql); + + // var ids = pagedResult.Items.Select(x => (int)x.id).InGroupsOf(2000); + // var entities = pagedResult.Items.Select(BuildEntityFromDynamic).Cast().ToList(); + + // //Now we need to merge in the property data since we need paging and we can't do this the way that the big media query was working before + // foreach (var idGroup in ids) + // { + // var propSql = GetPropertySql(Constants.ObjectTypes.Media) + // .WhereIn(x => x.NodeId, idGroup) + // .OrderBy(x => x.NodeId); + + // //This does NOT fetch all data into memory in a list, this will read + // // over the records as a data reader, this is much better for performance and memory, + // // but it means that during the reading of this data set, nothing else can be read + // // from SQL server otherwise we'll get an exception. + // var allPropertyData = UnitOfWork.Database.Query(propSql); + + // //keep track of the current property data item being enumerated + // var propertyDataSetEnumerator = allPropertyData.GetEnumerator(); + // var hasCurrent = false; // initially there is no enumerator.Current + + // try + // { + // //This must be sorted by node id (which is done by SQL) because this is how we are sorting the query to lookup property types above, + // // which allows us to more efficiently iterate over the large data set of property values. + // foreach (var entity in entities) + // { + // // assemble the dtos for this def + // // use the available enumerator.Current if any else move to next + // while (hasCurrent || propertyDataSetEnumerator.MoveNext()) + // { + // if (propertyDataSetEnumerator.Current.nodeId == entity.Id) + // { + // hasCurrent = false; // enumerator.Current is not available + + // //the property data goes into the additional data + // entity.AdditionalData[propertyDataSetEnumerator.Current.propertyTypeAlias] = new UmbracoEntity.EntityProperty + // { + // PropertyEditorAlias = propertyDataSetEnumerator.Current.propertyEditorAlias, + // Value = StringExtensions.IsNullOrWhiteSpace(propertyDataSetEnumerator.Current.textValue) + // ? propertyDataSetEnumerator.Current.varcharValue + // : StringExtensions.ConvertToJsonIfPossible(propertyDataSetEnumerator.Current.textValue) + // }; + // } + // else + // { + // hasCurrent = true; // enumerator.Current is available for another def + // break; // no more propertyDataDto for this def + // } + // } + // } + // } + // finally + // { + // propertyDataSetEnumerator.Dispose(); + // } + // } + + // result = entities; + //} + //else + //{ + // var pagedResult = UnitOfWork.Database.Page(pageIndex + 1, pageSize, pagedSql); + // result = pagedResult.Items.Select(BuildEntityFromDynamic).Cast().ToList(); + //} + + var page = Database.Page(pageIndex + 1, pageSize, sql); + var dtos = page.Items; + var entities = dtos.Select(x => BuildEntity(isContent, isMedia, x)).ToArray(); if (isMedia) - { - //Treat media differently for now, as an Entity it will be returned with ALL of it's properties in the AdditionalData bag! - var pagedResult = UnitOfWork.Database.Page(pageIndex + 1, pageSize, pagedSql); + BuildProperties(entities, dtos); - var ids = pagedResult.Items.Select(x => (int)x.id).InGroupsOf(2000); - var entities = pagedResult.Items.Select(factory.BuildEntityFromDynamic).Cast().ToList(); - - //Now we need to merge in the property data since we need paging and we can't do this the way that the big media query was working before - foreach (var idGroup in ids) - { - var propSql = GetPropertySql(Constants.ObjectTypes.Media) - .WhereIn(x => x.NodeId, idGroup) - .OrderBy(x => x.NodeId); - - //This does NOT fetch all data into memory in a list, this will read - // over the records as a data reader, this is much better for performance and memory, - // but it means that during the reading of this data set, nothing else can be read - // from SQL server otherwise we'll get an exception. - var allPropertyData = UnitOfWork.Database.Query(propSql); - - //keep track of the current property data item being enumerated - var propertyDataSetEnumerator = allPropertyData.GetEnumerator(); - var hasCurrent = false; // initially there is no enumerator.Current - - try - { - //This must be sorted by node id (which is done by SQL) because this is how we are sorting the query to lookup property types above, - // which allows us to more efficiently iterate over the large data set of property values. - foreach (var entity in entities) - { - // assemble the dtos for this def - // use the available enumerator.Current if any else move to next - while (hasCurrent || propertyDataSetEnumerator.MoveNext()) - { - if (propertyDataSetEnumerator.Current.nodeId == entity.Id) - { - hasCurrent = false; // enumerator.Current is not available - - //the property data goes into the additional data - entity.AdditionalData[propertyDataSetEnumerator.Current.propertyTypeAlias] = new UmbracoEntity.EntityProperty - { - PropertyEditorAlias = propertyDataSetEnumerator.Current.propertyEditorAlias, - Value = StringExtensions.IsNullOrWhiteSpace(propertyDataSetEnumerator.Current.textValue) - ? propertyDataSetEnumerator.Current.varcharValue - : StringExtensions.ConvertToJsonIfPossible(propertyDataSetEnumerator.Current.textValue) - }; - } - else - { - hasCurrent = true; // enumerator.Current is available for another def - break; // no more propertyDataDto for this def - } - } - } - } - finally - { - propertyDataSetEnumerator.Dispose(); - } - } - - result = entities; - } - else - { - var pagedResult = UnitOfWork.Database.Page(pageIndex + 1, pageSize, pagedSql); - result = pagedResult.Items.Select(factory.BuildEntityFromDynamic).Cast().ToList(); - } - - //The total items from the PetaPoco page query will be wrong due to the Outer join used on parent, depending on the search this will - //return duplicate results when the COUNT is used in conjuction with it, so we need to get the total on our own. - - //generate a query that does not contain the LEFT Join for parent, this would cause - //the COUNT(*) query to return the wrong - var sqlCountClause = GetBaseWhere( - (isC, isM, f) => GetBase(isC, isM, f, true), //true == is a count query - isContent, isMedia, sql => - { - if (filter != null) - { - foreach (var filterClause in filter.GetWhereClauses()) - { - sql.Where(filterClause.Item1, filterClause.Item2); - } - } - }, objectTypeId); - var translatorCount = new SqlTranslator(sqlCountClause, query); - var countSql = translatorCount.Translate(); - - totalRecords = UnitOfWork.Database.ExecuteScalar(countSql); - - return result; + totalRecords = page.TotalItems; + return entities; } public IUmbracoEntity GetByKey(Guid key) { - var sql = GetBaseWhere(GetBase, false, false, key); - var nodeDto = UnitOfWork.Database.FirstOrDefault(sql); - if (nodeDto == null) - return null; - - var factory = new UmbracoEntityFactory(); - var entity = factory.BuildEntityFromDynamic(nodeDto); - - return entity; + var sql = GetBaseWhere(false, false, false, key); + var dto = Database.FirstOrDefault(sql); + return dto == null ? null : BuildEntity(false, false, dto); } public IUmbracoEntity GetByKey(Guid key, Guid objectTypeId) @@ -168,51 +147,23 @@ namespace Umbraco.Core.Persistence.Repositories var isContent = objectTypeId == Constants.ObjectTypes.Document || objectTypeId == Constants.ObjectTypes.DocumentBlueprint; var isMedia = objectTypeId == Constants.ObjectTypes.Media; - var sql = GetFullSqlForEntityType(key, isContent, isMedia, objectTypeId); + var sql = GetFullSqlForEntityType(isContent, isMedia, objectTypeId, key); + var dto = Database.FirstOrDefault(sql); + if (dto == null) return null; + + var entity = BuildEntity(isContent, isMedia, dto); if (isMedia) - { - //for now treat media differently and include all property data too - return UnitOfWork.Database - .Fetch(sql) - .Transform(new UmbracoEntityRelator().MapAll) - .FirstOrDefault(); - } - - // else - - // query = read forward data reader, do not load everything into mem - // fixme wtf is this collection thing?! - var dtos = UnitOfWork.Database.Query(sql); - var collection = new EntityDefinitionCollection(); - var factory = new UmbracoEntityFactory(); - foreach (var dto in dtos) - { - collection.AddOrUpdate(new EntityDefinition(factory, dto, isContent, false)); - } - var found = collection.FirstOrDefault(); - return found != null ? found.BuildFromDynamic() : null; - - var nodeDto = UnitOfWork.Database.FirstOrDefault(sql); - if (nodeDto == null) - return null; - - var entity = factory.BuildEntityFromDynamic(nodeDto); + BuildProperties(entity, dto); return entity; } public virtual IUmbracoEntity Get(int id) { - var sql = GetBaseWhere(GetBase, false, false, id); - var nodeDto = UnitOfWork.Database.FirstOrDefault(sql); - if (nodeDto == null) - return null; - - var factory = new UmbracoEntityFactory(); - var entity = factory.BuildEntityFromDynamic(nodeDto); - - return entity; + var sql = GetBaseWhere(false, false, false, id); + var dto = Database.FirstOrDefault(sql); + return dto == null ? null : BuildEntity(false, false, dto); } public virtual IUmbracoEntity Get(int id, Guid objectTypeId) @@ -220,405 +171,397 @@ namespace Umbraco.Core.Persistence.Repositories var isContent = objectTypeId == Constants.ObjectTypes.Document || objectTypeId == Constants.ObjectTypes.DocumentBlueprint; var isMedia = objectTypeId == Constants.ObjectTypes.Media; - var sql = GetFullSqlForEntityType(id, isContent, isMedia, objectTypeId); + var sql = GetFullSqlForEntityType(isContent, isMedia, objectTypeId, id); + var dto = Database.FirstOrDefault(sql); + if (dto == null) return null; + + var entity = BuildEntity(isContent, isMedia, dto); if (isMedia) - { - //for now treat media differently and include all property data too - return UnitOfWork.Database - .Fetch(sql) - .Transform(new UmbracoEntityRelator().MapAll) - .FirstOrDefault(); - } + BuildProperties(entity, dto); - // else - - //query = read forward data reader, do not load everything into mem - var dtos = UnitOfWork.Database.Query(sql); - var collection = new EntityDefinitionCollection(); - var factory = new UmbracoEntityFactory(); - foreach (var dto in dtos) - { - collection.AddOrUpdate(new EntityDefinition(factory, dto, isContent, false)); - } - - var found = collection.FirstOrDefault(); - return found?.BuildFromDynamic(); + return entity; } - public virtual IEnumerable GetAll(Guid objectTypeId, params int[] ids) + public virtual IEnumerable GetAll(Guid objectType, params int[] ids) { - return ids.Any() - ? PerformGetAll(objectTypeId, sql => sql.Where(" umbracoNode.id in (@ids)", new { ids })) - : PerformGetAll(objectTypeId); + return ids.Length > 0 + ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) + : PerformGetAll(objectType); } - public virtual IEnumerable GetAll(Guid objectTypeId, params Guid[] keys) + public virtual IEnumerable GetAll(Guid objectType, params Guid[] keys) { - return keys.Any() - ? PerformGetAll(objectTypeId, sql => sql.Where(" umbracoNode.uniqueId in (@keys)", new { keys })) - : PerformGetAll(objectTypeId); + return keys.Length > 0 + ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) + : PerformGetAll(objectType); } - private IEnumerable PerformGetAll(Guid objectTypeId, Action filter = null) + private IEnumerable PerformGetAll(Guid objectType, Action> filter = null) { - var isContent = objectTypeId == Constants.ObjectTypes.Document || objectTypeId == Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectTypeId == Constants.ObjectTypes.Media; - var sql = GetFullSqlForEntityType(isContent, isMedia, objectTypeId, filter); + var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectType == Constants.ObjectTypes.Media; + + var sql = GetFullSqlForEntityType(isContent, isMedia, objectType, filter); + var dtos = Database.Fetch(sql); + if (dtos.Count == 0) return Enumerable.Empty(); + + var entities = dtos.Select(x => BuildEntity(isContent, isMedia, x)).ToArray(); if (isMedia) - { - //for now treat media differently and include all property data too - return UnitOfWork.Database - .Fetch(sql) - .Transform(new UmbracoEntityRelator().MapAll); - } + BuildProperties(entities, dtos); - //query = read forward data reader, do not load everything into mem - var dtos = UnitOfWork.Database.Query(sql); - var collection = new EntityDefinitionCollection(); - var factory = new UmbracoEntityFactory(); - foreach (var dto in dtos) - { - collection.AddOrUpdate(new EntityDefinition(factory, dto, isContent, false)); - } - return collection.Select(x => x.BuildFromDynamic()).ToList(); + return entities; } - public virtual IEnumerable GetAllPaths(Guid objectTypeId, params int[] ids) + public virtual IEnumerable GetAllPaths(Guid objectType, params int[] ids) { return ids.Any() - ? PerformGetAllPaths(objectTypeId, sql => sql.Append(" AND umbracoNode.id in (@ids)", new { ids })) - : PerformGetAllPaths(objectTypeId); + ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) + : PerformGetAllPaths(objectType); } - public virtual IEnumerable GetAllPaths(Guid objectTypeId, params Guid[] keys) + public virtual IEnumerable GetAllPaths(Guid objectType, params Guid[] keys) { return keys.Any() - ? PerformGetAllPaths(objectTypeId, sql => sql.Append(" AND umbracoNode.uniqueID in (@keys)", new { keys })) - : PerformGetAllPaths(objectTypeId); + ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) + : PerformGetAllPaths(objectType); } - private IEnumerable PerformGetAllPaths(Guid objectTypeId, Action filter = null) + private IEnumerable PerformGetAllPaths(Guid objectType, Action> filter = null) { - var sql = new Sql("SELECT id, path FROM umbracoNode WHERE umbracoNode.nodeObjectType=@type", new { type = objectTypeId }); + var sql = Sql().Select(x => x.NodeId, x => x.Path).From().Where(x => x.NodeObjectType == objectType); filter?.Invoke(sql); - return UnitOfWork.Database.Fetch(sql); + return Database.Fetch(sql); } public virtual IEnumerable GetByQuery(IQuery query) { var sqlClause = GetBase(false, false, null); var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate().Append(GetGroupBy(false, false)); - - var dtos = UnitOfWork.Database.Fetch(sql); - - var factory = new UmbracoEntityFactory(); - var list = dtos.Select(factory.BuildEntityFromDynamic).Cast().ToList(); - - return list; + var sql = translator.Translate(); + sql = AddGroupBy(false, false, sql); + var dtos = Database.Fetch(sql); + return dtos.Select(x => BuildEntity(false, false, x)).ToList(); } - public virtual IEnumerable GetByQuery(IQuery query, Guid objectTypeId) + public virtual IEnumerable GetByQuery(IQuery query, Guid objectType) { - var isContent = objectTypeId == Constants.ObjectTypes.Document || objectTypeId == Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectTypeId == Constants.ObjectTypes.Media; + var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectType == Constants.ObjectTypes.Media; - var sqlClause = GetBaseWhere(GetBase, isContent, isMedia, null, objectTypeId); + var sql = GetBaseWhere(isContent, isMedia, false, null, objectType); + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + sql = AddGroupBy(isContent, isMedia, sql); + var dtos = Database.Fetch(sql); + if (dtos.Count == 0) return Enumerable.Empty(); - var translator = new SqlTranslator(sqlClause, query); - var entitySql = translator.Translate(); + var entities = dtos.Select(x => BuildEntity(isContent, isMedia, x)).ToArray(); if (isMedia) - { - var wheres = query.GetWhereClauses().ToArray(); + BuildProperties(entities, dtos); - var mediaSql = GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false)), sql => - { - //adds the additional filters - foreach (var whereClause in wheres) - { - sql.Where(whereClause.Item1, whereClause.Item2); - } - }); - - //for now treat media differently and include all property data too - return UnitOfWork.Database - .Fetch(mediaSql) - .Transform(new UmbracoEntityRelator().MapAll); - } - - // else - - //use dynamic so that we can get ALL properties from the SQL so we can chuck that data into our AdditionalData - var finalSql = entitySql.Append(GetGroupBy(isContent, false)); - //query = read forward data reader, do not load everything into mem - var dtos = UnitOfWork.Database.Query(finalSql); - var collection = new EntityDefinitionCollection(); - var factory = new UmbracoEntityFactory(); - foreach (var dto in dtos) - { - collection.AddOrUpdate(new EntityDefinition(factory, dto, isContent, false)); - } - return collection.Select(x => x.BuildFromDynamic()).ToList(); + return entities; } public UmbracoObjectTypes GetObjectType(int id) { - var sql = Sql().Select("nodeObjectType").From().Where(x => x.NodeId == id); - var nodeObjectTypeId = UnitOfWork.Database.ExecuteScalar(sql); - var objectTypeId = nodeObjectTypeId; - return UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); + var sql = Sql().Select(x => x.NodeObjectType).From().Where(x => x.NodeId == id); + return UmbracoObjectTypesExtensions.GetUmbracoObjectType(Database.ExecuteScalar(sql)); } public UmbracoObjectTypes GetObjectType(Guid key) { - var sql = Sql().Select("nodeObjectType").From().Where(x => x.UniqueId == key); - var nodeObjectTypeId = UnitOfWork.Database.ExecuteScalar(sql); - var objectTypeId = nodeObjectTypeId; - return UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); + var sql = Sql().Select(x => x.NodeObjectType).From().Where(x => x.UniqueId == key); + return UmbracoObjectTypesExtensions.GetUmbracoObjectType(Database.ExecuteScalar(sql)); + } + + public bool Exists(Guid key) + { + var sql = Sql().SelectCount().From().Where(x => x.UniqueId == key); + return Database.ExecuteScalar(sql) > 0; + } + + public bool Exists(int id) + { + var sql = Sql().SelectCount().From().Where(x => x.NodeId == id); + return Database.ExecuteScalar(sql) > 0; + } + + private void BuildProperties(UmbracoEntity entity, BaseDto dto) + { + var pdtos = Database.Fetch(GetPropertyData(dto.VersionId)); + foreach (var pdto in pdtos) + BuildProperty(entity, pdto); + } + + private void BuildProperties(UmbracoEntity[] entities, List dtos) + { + var versionIds = dtos.Select(x => x.VersionId).Distinct().ToArray(); + var pdtos = Database.FetchByGroups(versionIds, 2000, GetPropertyData); + + var xentity = entities.ToDictionary(x => x.Id, x => x); // nodeId -> entity + var xdto = dtos.ToDictionary(x => x.VersionId, x => x.NodeId); // versionId -> nodeId + foreach (var pdto in pdtos) + { + var nodeId = xdto[pdto.VersionId]; + var entity = xentity[nodeId]; + BuildProperty(entity, pdto); + } + } + + private void BuildProperty(UmbracoEntity entity, PropertyDataDto pdto) + { + // explain ?! + var value = string.IsNullOrWhiteSpace(pdto.TextValue) + ? pdto.VarcharValue + : pdto.TextValue.ConvertToJsonIfPossible(); + + entity.AdditionalData[pdto.PropertyTypeDto.Alias] = new UmbracoEntity.EntityProperty + { + PropertyEditorAlias = pdto.PropertyTypeDto.DataTypeDto.PropertyEditorAlias, + Value = value + }; } #endregion #region Sql - protected Sql GetFullSqlForEntityType(Guid key, bool isContent, bool isMedia, Guid objectTypeId) + // gets the full sql for a given object type and a given unique id + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Guid uniqueId) { - var entitySql = GetBaseWhere(GetBase, isContent, isMedia, objectTypeId, key); - - return isMedia - ? GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false))) - : entitySql.Append(GetGroupBy(isContent, false)); + var sql = GetBaseWhere(isContent, isMedia, false, objectType, uniqueId); + return AddGroupBy(isContent, isMedia, sql); } - protected Sql GetFullSqlForEntityType(int id, bool isContent, bool isMedia, Guid objectTypeId) + // gets the full sql for a given object type and a given node id + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, int nodeId) { - var entitySql = GetBaseWhere(GetBase, isContent, isMedia, objectTypeId, id); - - return isMedia - ? GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false))) - : entitySql.Append(GetGroupBy(isContent, false)); + var sql = GetBaseWhere(isContent, isMedia, false, objectType, nodeId); + return AddGroupBy(isContent, isMedia, sql); } - protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectTypeId, Action> filter) + // gets the full sql for a given object type, with a given filter + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Action> filter) { - var entitySql = GetBaseWhere(GetBase, isContent, isMedia, filter, objectTypeId); - - return isMedia - ? GetFullSqlForMedia(entitySql.Append(GetGroupBy(isContent, true, false)), filter) - : entitySql.Append(GetGroupBy(isContent, false)); + var sql = GetBaseWhere(isContent, isMedia, false, filter, objectType); + return AddGroupBy(isContent, isMedia, sql); } - private Sql GetPropertySql(Guid nodeObjectType) + // fixme kill this nonsense + //// gets the SELECT + FROM + WHERE sql + //// to get all property data for all items of the specified object type + //private Sql GetPropertySql(Guid objectType) + //{ + // return Sql() + // .Select(x => x.VersionId, x => x.TextValue, x => x.VarcharValue) + // .AndSelect(x => x.NodeId) + // .AndSelect(x => x.PropertyEditorAlias) + // .AndSelect(x => Alias(x.Alias, "propertyTypeAlias")) + // .From() + // .InnerJoin().On((left, right) => left.VersionId == right.Id) + // .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + // .InnerJoin().On(dto => dto.PropertyTypeId, dto => dto.Id) + // .InnerJoin().On(dto => dto.DataTypeId, dto => dto.DataTypeId) + // .Where(x => x.NodeObjectType == objectType); + //} + + private Sql GetPropertyData(int versionId) { - var sql = Sql() - .Select(x => x.VersionId, x => x.TextValue, x => x.VarcharValue) - .AndSelect(x => x.PropertyEditorAlias) - .AndSelectAs(x => x.Alias, "propertyTypeAlias") + return Sql() + .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x.DataTypeDto))) .From() - .InnerJoin().On((left, right) => left.VersionId == right.Id) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On(dto => dto.PropertyTypeId, dto => dto.Id) - .InnerJoin().On(dto => dto.DataTypeId, dto => dto.DataTypeId) - .Where(x => x.NodeObjectType == nodeObjectType); - - return sql; + .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id) + .InnerJoin().On((left, right) => left.DataTypeId == right.DataTypeId) + .Where(x => x.VersionId == versionId); } - private Sql GetFullSqlForMedia(Sql entitySql, Action> filter = null) + private Sql GetPropertyData(IEnumerable versionIds) { - //this will add any varcharValue property to the output which can be added to the additional properties - - var sql = GetPropertySql(Constants.ObjectTypes.Media); - - filter?.Invoke(sql); - - // We're going to create a query to query against the entity SQL - // because we cannot group by nText columns and we have a COUNT in the entitySql we cannot simply left join - // the entitySql query, we have to join the wrapped query to get the ntext in the result - - var wrappedSql = Sql() - .Append("SELECT * FROM (") - .Append(entitySql) - .Append(") tmpTbl LEFT JOIN (") - .Append(sql) - .Append(") as property ON id = property.nodeId") - .OrderBy("sortOrder, id"); - - return wrappedSql; + return Sql() + .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x.DataTypeDto))) + .From() + .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id) + .InnerJoin().On((left, right) => left.DataTypeId == right.DataTypeId) + .WhereIn(x => x.VersionId, versionIds) + .OrderBy(x => x.VersionId); } - protected virtual Sql GetBase(bool isContent, bool isMedia, Action> customFilter) + // fixme - wtf is this? + //private Sql GetFullSqlForMedia(Sql entitySql, Action> filter = null) + //{ + // //this will add any varcharValue property to the output which can be added to the additional properties + + // var sql = GetPropertySql(Constants.ObjectTypes.Media); + + // filter?.Invoke(sql); + + // // We're going to create a query to query against the entity SQL + // // because we cannot group by nText columns and we have a COUNT in the entitySql we cannot simply left join + // // the entitySql query, we have to join the wrapped query to get the ntext in the result + + // var wrappedSql = Sql() + // .Append("SELECT * FROM (") + // .Append(entitySql) + // .Append(") tmpTbl LEFT JOIN (") + // .Append(sql) + // .Append(") as property ON id = property.nodeId") + // .OrderBy("sortOrder, id"); + + // return wrappedSql; + //} + + // the DTO corresponding to fields selected by GetBase + // ReSharper disable once ClassNeverInstantiated.Local + private class BaseDto { - return GetBase(isContent, isMedia, customFilter, false); + // ReSharper disable UnusedAutoPropertyAccessor.Local + // ReSharper disable UnusedMember.Local + public int NodeId { get; set; } + public bool Trashed { get; set; } + public int ParentId { get; set; } + public int? UserId { get; set; } + public int Level { get; set; } + public string Path { get; set; } + public int SortOrder { get; set; } + public Guid UniqueId { get; set; } + public string Text { get; set; } + public Guid NodeObjectType { get; set; } + public DateTime CreateDate { get; set; } + public int Children { get; set; } + public int VersionId { get; set; } + public bool Published { get; set; } + public bool Edited { get; set; } + public string Alias { get; set; } + public string Icon { get; set; } + public string Thumbnail { get; set; } + public bool IsContainer { get; set; } + // ReSharper restore UnusedAutoPropertyAccessor.Local + // ReSharper restore UnusedMember.Local } - protected virtual Sql GetBase(bool isContent, bool isMedia, Action> customFilter, bool isCount) + // gets the base SELECT + FROM [+ filter] sql + // always from the 'current' content version + protected virtual Sql GetBase(bool isContent, bool isMedia, Action> filter, bool isCount = false) { - var columns = new List(); + var sql = Sql(); if (isCount) { - columns.Add("COUNT(*)"); + sql.SelectCount(); } else { - columns.AddRange(new[] - { - "umbracoNode.id", - "umbracoNode.trashed", - "umbracoNode.parentID", - "umbracoNode.nodeUser", - "umbracoNode.level", - "umbracoNode.path", - "umbracoNode.sortOrder", - "umbracoNode.uniqueID", - "umbracoNode.text", - "umbracoNode.nodeObjectType", - "umbracoNode.createDate", - "COUNT(child.id) as children" - }); + sql + .Select(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path) + .AndSelect(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, x => x.CreateDate) + .Append(", COUNT(child.id) AS children"); if (isContent) - { - columns.Add("contentVersion.versionId as versionId"); - columns.Add("document.published"); - columns.Add("document.edited"); - } + sql + .AndSelect(x => x.Published, x => x.Edited); if (isContent || isMedia) - { - columns.Add("contentType.alias"); - columns.Add("contentType.icon"); - columns.Add("contentType.thumbnail"); - columns.Add("contentType.isContainer"); - } + sql + .AndSelect(x => Alias(x.Id, "versionId")) + .AndSelect(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer); } - //Creates an SQL query to return a single row for the entity - - var entitySql = Sql() - .Select(columns.ToArray()) + sql .From(); if (isContent || isMedia) { - entitySql + sql + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .LeftJoin("contentType").On((left, right) => left.ContentTypeId == right.NodeId, aliasRight: "contentType"); + .LeftJoin().On((left, right) => left.ContentTypeId == right.NodeId); } if (isContent) { - entitySql - .InnerJoin("contentVersion").On((left, right) => left.NodeId == right.NodeId && right.Current, aliasRight: "contentVersion") - .InnerJoin("document").On((left, right) => left.NodeId == right.NodeId, aliasRight: "document"); + sql + .InnerJoin().On((left, right) => left.NodeId == right.NodeId); } if (isCount == false) - entitySql + sql .LeftJoin("child").On((left, right) => left.NodeId == right.ParentId, aliasRight: "child"); - customFilter?.Invoke(entitySql); + filter?.Invoke(sql); - return entitySql; - } - - protected virtual Sql GetBaseWhere(Func>, Sql> baseQuery, bool isContent, bool isMedia, Action> filter, Guid nodeObjectType) - { - var sql = baseQuery(isContent, isMedia, filter) - .Where(x => x.NodeObjectType == nodeObjectType); return sql; } - protected virtual Sql GetBaseWhere(Func>, Sql> baseQuery, bool isContent, bool isMedia, int id) + // gets the base SELECT + FROM [+ filter] + WHERE sql + // for a given object type, with a given filter + protected virtual Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Action> filter, Guid objectType) { - var sql = baseQuery(isContent, isMedia, null) + return GetBase(isContent, isMedia, filter, isCount) + .Where(x => x.NodeObjectType == objectType); + } + + // gets the base SELECT + FROM + WHERE sql + // for a given node id + protected virtual Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, int id) + { + var sql = GetBase(isContent, isMedia, null, isCount) .Where(x => x.NodeId == id); - sql.Append(GetGroupBy(isContent, isMedia)); - return sql; + return AddGroupBy(isContent, isMedia, sql); } - protected virtual Sql GetBaseWhere(Func>, Sql> baseQuery, bool isContent, bool isMedia, Guid key) + // gets the base SELECT + FROM + WHERE sql + // for a given unique id + protected virtual Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid uniqueId) { - var sql = baseQuery(isContent, isMedia, null) - .Where(x => x.UniqueId == key); - sql.Append(GetGroupBy(isContent, isMedia)); - return sql; + var sql = GetBase(isContent, isMedia, null, isCount) + .Where(x => x.UniqueId == uniqueId); + return AddGroupBy(isContent, isMedia, sql); } - protected virtual Sql GetBaseWhere(Func>, Sql> baseQuery, bool isContent, bool isMedia, Guid nodeObjectType, int id) + // gets the base SELECT + FROM + WHERE sql + // for a given object type and node id + protected virtual Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid objectType, int nodeId) { - var sql = baseQuery(isContent, isMedia, null) - .Where(x => x.NodeId == id && x.NodeObjectType == nodeObjectType); - return sql; + return GetBase(isContent, isMedia, null, isCount) + .Where(x => x.NodeId == nodeId && x.NodeObjectType == objectType); } - protected virtual Sql GetBaseWhere(Func>, Sql> baseQuery, bool isContent, bool isMedia, Guid nodeObjectType, Guid key) + // gets the base SELECT + FROM + WHERE sql + // for a given object type and unique id + protected virtual Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid objectType, Guid uniqueId) { - var sql = baseQuery(isContent, isMedia, null) - .Where(x => x.UniqueId == key && x.NodeObjectType == nodeObjectType); - return sql; + return GetBase(isContent, isMedia, null, isCount) + .Where(x => x.UniqueId == uniqueId && x.NodeObjectType == objectType); } - protected virtual Sql GetGroupBy(bool isContent, bool isMedia, bool includeSort = true) + // gets the GROUP BY / ORDER BY sql + // required in order to count children + protected virtual Sql AddGroupBy(bool isContent, bool isMedia, Sql sql, bool sort = true) { - var columns = new List - { - "umbracoNode.id", - "umbracoNode.trashed", - "umbracoNode.parentId", - "umbracoNode.nodeUser", - "umbracoNode.level", - "umbracoNode.path", - "umbracoNode.sortOrder", - "umbracoNode.uniqueId", - "umbracoNode.text", - "umbracoNode.nodeObjectType", - "umbracoNode.createDate" - }; + sql + .GroupBy(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path) + .AndBy(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, x => x.CreateDate); if (isContent) - { - columns.Add("contentVersion.versionId"); - columns.Add("document.published"); - columns.Add("document.edited"); - } + sql + .AndBy(x => x.Published, x => x.Edited); if (isContent || isMedia) - { - columns.Add("contentType.alias"); - columns.Add("contentType.icon"); - columns.Add("contentType.thumbnail"); - columns.Add("contentType.isContainer"); - } + sql + .AndBy(x => x.Id) + .AndBy(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer); - var sql = Sql().GroupBy(columns.ToArray()); - - if (includeSort) - { - sql = sql.OrderBy("umbracoNode.sortOrder"); - } + if (sort) + sql.OrderBy(x => x.SortOrder); return sql; } #endregion - public bool Exists(Guid key) - { - var sql = Sql().SelectCount().From().Where(x => x.UniqueId == key); - return UnitOfWork.Database.ExecuteScalar(sql) > 0; - } - - public bool Exists(int id) - { - var sql = Sql().SelectCount().From().Where(x => x.NodeId == id); - return UnitOfWork.Database.ExecuteScalar(sql) > 0; - } - #region Classes [ExplicitColumns] @@ -637,6 +580,7 @@ namespace Umbraco.Core.Persistence.Repositories public string TextValue { get; set; } } + // fixme kill /// /// This is a special relator in that it is not returning a DTO but a real resolved entity and that it accepts /// a dynamic instance. @@ -645,175 +589,276 @@ namespace Umbraco.Core.Persistence.Repositories /// We're doing this because when we query the db, we want to use dynamic so that it returns all available fields not just the ones /// defined on the entity so we can them to additional data /// - internal class UmbracoEntityRelator - { - internal UmbracoEntity Current; - private readonly UmbracoEntityFactory _factory = new UmbracoEntityFactory(); + //internal class UmbracoEntityRelator + //{ + // internal UmbracoEntity Current; - public IEnumerable MapAll(IEnumerable input) - { - UmbracoEntity entity; + // public IEnumerable MapAll(IEnumerable input) + // { + // UmbracoEntity entity; - foreach (var x in input) - { - entity = Map(x); - if (entity != null) yield return entity; - } + // foreach (var x in input) + // { + // entity = Map(x); + // if (entity != null) yield return entity; + // } - entity = Map((dynamic) null); - if (entity != null) yield return entity; - } + // entity = Map((dynamic) null); + // if (entity != null) yield return entity; + // } - // must be called one last time with null in order to return the last one! - public UmbracoEntity Map(dynamic a) - { - // Terminating call. Since we can return null from this function - // we need to be ready for NPoco to callback later with null - // parameters - if (a == null) - return Current; + // // must be called one last time with null in order to return the last one! + // public UmbracoEntity Map(dynamic a) + // { + // // Terminating call. Since we can return null from this function + // // we need to be ready for NPoco to callback later with null + // // parameters + // if (a == null) + // return Current; - string pPropertyEditorAlias = a.propertyEditorAlias; - var pExists = pPropertyEditorAlias != null; - string pPropertyAlias = a.propertyTypeAlias; - string pTextValue = a.textValue; - string pNVarcharValue = a.varcharValue; + // string pPropertyEditorAlias = a.propertyEditorAlias; + // var pExists = pPropertyEditorAlias != null; + // string pPropertyAlias = a.propertyTypeAlias; + // string pTextValue = a.textValue; + // string pNVarcharValue = a.varcharValue; - // Is this the same UmbracoEntity as the current one we're processing - if (Current != null && Current.Key == a.uniqueID) - { - if (pExists && pPropertyAlias.IsNullOrWhiteSpace() == false) - { - // Add this UmbracoProperty to the current additional data - Current.AdditionalData[pPropertyAlias] = new UmbracoEntity.EntityProperty - { - PropertyEditorAlias = pPropertyEditorAlias, - Value = pTextValue.IsNullOrWhiteSpace() - ? pNVarcharValue - : pTextValue.ConvertToJsonIfPossible() - }; - } + // // Is this the same UmbracoEntity as the current one we're processing + // if (Current != null && Current.Key == a.uniqueID) + // { + // if (pExists && pPropertyAlias.IsNullOrWhiteSpace() == false) + // { + // // Add this UmbracoProperty to the current additional data + // Current.AdditionalData[pPropertyAlias] = new UmbracoEntity.EntityProperty + // { + // PropertyEditorAlias = pPropertyEditorAlias, + // Value = pTextValue.IsNullOrWhiteSpace() + // ? pNVarcharValue + // : pTextValue.ConvertToJsonIfPossible() + // }; + // } - // Return null to indicate we're not done with this UmbracoEntity yet - return null; - } + // // Return null to indicate we're not done with this UmbracoEntity yet + // return null; + // } - // This is a different UmbracoEntity to the current one, or this is the - // first time through and we don't have a Tab yet + // // This is a different UmbracoEntity to the current one, or this is the + // // first time through and we don't have a Tab yet - // Save the current UmbracoEntityDto - var prev = Current; + // // Save the current UmbracoEntityDto + // var prev = Current; - // Setup the new current UmbracoEntity + // // Setup the new current UmbracoEntity - Current = _factory.BuildEntityFromDynamic(a); + // Current = BuildEntityFromDynamic(a); - if (pExists && pPropertyAlias.IsNullOrWhiteSpace() == false) - { - //add the property/create the prop list if null - Current.AdditionalData[pPropertyAlias] = new UmbracoEntity.EntityProperty - { - PropertyEditorAlias = pPropertyEditorAlias, - Value = pTextValue.IsNullOrWhiteSpace() - ? pNVarcharValue - : pTextValue.ConvertToJsonIfPossible() - }; - } + // if (pExists && pPropertyAlias.IsNullOrWhiteSpace() == false) + // { + // //add the property/create the prop list if null + // Current.AdditionalData[pPropertyAlias] = new UmbracoEntity.EntityProperty + // { + // PropertyEditorAlias = pPropertyEditorAlias, + // Value = pTextValue.IsNullOrWhiteSpace() + // ? pNVarcharValue + // : pTextValue.ConvertToJsonIfPossible() + // }; + // } - // Return the now populated previous UmbracoEntity (or null if first time through) - return prev; - } - } + // // Return the now populated previous UmbracoEntity (or null if first time through) + // return prev; + // } + //} // fixme need to review what's below // comes from 7.6, similar to what's in VersionableRepositoryBase // not sure it really makes sense... - private class EntityDefinitionCollection : KeyedCollection + //private class EntityDefinitionCollection : KeyedCollection + //{ + // protected override int GetKeyForItem(EntityDefinition item) + // { + // return item.Id; + // } + + // /// + // /// if this key already exists if it does then we need to check + // /// if the existing item is 'older' than the new item and if that is the case we'll replace the older one + // /// + // /// + // /// + // public bool AddOrUpdate(EntityDefinition item) + // { + // if (Dictionary == null) + // { + // Add(item); + // return true; + // } + + // var key = GetKeyForItem(item); + // if (TryGetValue(key, out EntityDefinition found)) + // { + // //it already exists and it's older so we need to replace it + // if (item.VersionId > found.VersionId) + // { + // var currIndex = Items.IndexOf(found); + // if (currIndex == -1) + // throw new IndexOutOfRangeException("Could not find the item in the list: " + found.Id); + + // //replace the current one with the newer one + // SetItem(currIndex, item); + // return true; + // } + // //could not add or update + // return false; + // } + + // Add(item); + // return true; + // } + + // private bool TryGetValue(int key, out EntityDefinition val) + // { + // if (Dictionary != null) return Dictionary.TryGetValue(key, out val); + + // val = null; + // return false; + // } + //} + + // fixme wtf is this, why dynamics here, this is horrible !! + //private class EntityDefinition + //{ + // private readonly dynamic _entity; + // private readonly bool _isContent; + // private readonly bool _isMedia; + + // public EntityDefinition(dynamic entity, bool isContent, bool isMedia) + // { + // _entity = entity; + // _isContent = isContent; + // _isMedia = isMedia; + // } + + // public IUmbracoEntity BuildFromDynamic() + // { + // return BuildEntityFromDynamic(_entity); + // } + + // public int Id => _entity.id; + + // public int VersionId + // { + // get + // { + // if (_isContent || _isMedia) + // { + // return _entity.versionId; + // } + // return _entity.id; + // } + // } + //} + + #endregion + + #region Factory + + // fixme kill all this + //private static void AddAdditionalData(UmbracoEntity entity, IDictionary originalEntityProperties) + //{ + // var entityProps = typeof(IUmbracoEntity).GetPublicProperties().Select(x => x.Name).ToArray(); + + // // figure out what extra properties we have that are not on the IUmbracoEntity and add them to additional data + // foreach (var k in originalEntityProperties.Keys + // .Select(x => new { orig = x, title = x.ToCleanString(CleanStringType.PascalCase | CleanStringType.Ascii | CleanStringType.ConvertCase) }) + // .Where(x => entityProps.InvariantContains(x.title) == false)) + // { + // entity.AdditionalData[k.title] = originalEntityProperties[k.orig]; + // } + //} + + //private static UmbracoEntity BuildEntityFromDynamic(dynamic d) + //{ + // var asDictionary = (IDictionary) d; + // var entity = new UmbracoEntity(d.trashed); + + // try + // { + // entity.DisableChangeTracking(); + + // entity.CreateDate = d.createDate; + // entity.CreatorId = d.nodeUser == null ? 0 : d.nodeUser; + // entity.Id = d.id; + // entity.Key = d.uniqueID; + // entity.Level = d.level; + // entity.Name = d.text; + // entity.NodeObjectTypeId = d.nodeObjectType; + // entity.ParentId = d.parentID; + // entity.Path = d.path; + // entity.SortOrder = d.sortOrder; + // entity.HasChildren = d.children > 0; + + // entity.ContentTypeAlias = asDictionary.ContainsKey("alias") ? (d.alias ?? string.Empty) : string.Empty; + // entity.ContentTypeIcon = asDictionary.ContainsKey("icon") ? (d.icon ?? string.Empty) : string.Empty; + // entity.ContentTypeThumbnail = asDictionary.ContainsKey("thumbnail") ? (d.thumbnail ?? string.Empty) : string.Empty; + // //entity.VersionId = asDictionary.ContainsKey("versionId") ? asDictionary["versionId"] : Guid.Empty; + + // entity.Published = asDictionary.ContainsKey("published") && (bool) asDictionary["published"]; + // entity.Edited = asDictionary.ContainsKey("edited") && (bool) asDictionary["edited"]; + + // // assign the additional data + // AddAdditionalData(entity, asDictionary); + + // return entity; + // } + // finally + // { + // entity.EnableChangeTracking(); + // } + //} + + private static UmbracoEntity BuildEntity(bool isContent, bool isMedia, BaseDto dto) { - protected override int GetKeyForItem(EntityDefinition item) - { - return item.Id; - } + var entity = new UmbracoEntity(dto.Trashed); - /// - /// if this key already exists if it does then we need to check - /// if the existing item is 'older' than the new item and if that is the case we'll replace the older one - /// - /// - /// - public bool AddOrUpdate(EntityDefinition item) + try { - if (Dictionary == null) + entity.DisableChangeTracking(); + + entity.CreateDate = dto.CreateDate; + entity.CreatorId = dto.UserId ?? 0; + entity.Id = dto.NodeId; + entity.Key = dto.UniqueId; + entity.Level = dto.Level; + entity.Name = dto.Text; + entity.NodeObjectTypeId = dto.NodeObjectType; + entity.ParentId = dto.ParentId; + entity.Path = dto.Path; + entity.SortOrder = dto.SortOrder; + entity.HasChildren = dto.Children > 0; + + if (isContent) { - Add(item); - return true; + entity.Published = dto.Published; + entity.Edited = dto.Edited; } - var key = GetKeyForItem(item); - if (TryGetValue(key, out EntityDefinition found)) + // fixme what shall we do with versions? + //entity.VersionId = asDictionary.ContainsKey("versionId") ? asDictionary["versionId"] : Guid.Empty; + + if (isContent || isMedia) { - //it already exists and it's older so we need to replace it - if (item.VersionId > found.VersionId) - { - var currIndex = Items.IndexOf(found); - if (currIndex == -1) - throw new IndexOutOfRangeException("Could not find the item in the list: " + found.Id); - - //replace the current one with the newer one - SetItem(currIndex, item); - return true; - } - //could not add or update - return false; - } - - Add(item); - return true; - } - - private bool TryGetValue(int key, out EntityDefinition val) - { - if (Dictionary != null) return Dictionary.TryGetValue(key, out val); - - val = null; - return false; - } - } - - private class EntityDefinition - { - private readonly UmbracoEntityFactory _factory; - private readonly dynamic _entity; - private readonly bool _isContent; - private readonly bool _isMedia; - - public EntityDefinition(UmbracoEntityFactory factory, dynamic entity, bool isContent, bool isMedia) - { - _factory = factory; - _entity = entity; - _isContent = isContent; - _isMedia = isMedia; - } - - public IUmbracoEntity BuildFromDynamic() - { - return _factory.BuildEntityFromDynamic(_entity); - } - - public int Id => _entity.id; - - public int VersionId - { - get - { - if (_isContent || _isMedia) - { - return _entity.versionId; - } - return _entity.id; + entity.ContentTypeAlias = dto.Alias; + entity.ContentTypeIcon = dto.Icon; + entity.ContentTypeThumbnail = dto.Thumbnail; + //entity.??? = dto.IsContainer; } } + finally + { + entity.EnableChangeTracking(); + } + + return entity; } #endregion diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IEntityRepository.cs index 5893ce1c35..5a26e126f6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IEntityRepository.cs @@ -13,22 +13,22 @@ namespace Umbraco.Core.Persistence.Repositories IUmbracoEntity GetByKey(Guid key, Guid objectTypeId); IUmbracoEntity Get(int id); IUmbracoEntity Get(int id, Guid objectTypeId); - IEnumerable GetAll(Guid objectTypeId, params int[] ids); - IEnumerable GetAll(Guid objectTypeId, params Guid[] keys); + IEnumerable GetAll(Guid objectType, params int[] ids); + IEnumerable GetAll(Guid objectType, params Guid[] keys); IEnumerable GetByQuery(IQuery query); - IEnumerable GetByQuery(IQuery query, Guid objectTypeId); + IEnumerable GetByQuery(IQuery query, Guid objectType); UmbracoObjectTypes GetObjectType(int id); UmbracoObjectTypes GetObjectType(Guid key); - IEnumerable GetAllPaths(Guid objectTypeId, params int[] ids); - IEnumerable GetAllPaths(Guid objectTypeId, params Guid[] keys); + IEnumerable GetAllPaths(Guid objectType, params int[] ids); + IEnumerable GetAllPaths(Guid objectType, params Guid[] keys); /// /// Gets paged results /// /// Query to excute - /// + /// /// Page number /// Page size /// Total records query would return without paging @@ -36,7 +36,7 @@ namespace Umbraco.Core.Persistence.Repositories /// Direction to order by /// /// An Enumerable list of objects - IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectTypeId, long pageIndex, int pageSize, out long totalRecords, + IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, IQuery filter = null); /// diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 79eb5f7ada..65dc209067 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -192,7 +192,7 @@ namespace Umbraco.Core.Persistence.Repositories // If the stripped-down url returns null, we try again with the original url. // Previously, the function would fail on e.g. "my_x_image.jpg" - var nodeId = GetMediaNodeIdByPath(Sql().Where(x => x.VarcharValue== umbracoFileValue)); + var nodeId = GetMediaNodeIdByPath(Sql().Where(x => x.VarcharValue == umbracoFileValue)); if (nodeId < 0) nodeId = GetMediaNodeIdByPath(Sql().Where(x => x.VarcharValue == mediaPath)); // If no result so far, try getting from a json value stored in the ntext / nvarchar column @@ -207,7 +207,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = Sql().Select(x => x.NodeId) .From() .InnerJoin().On(left => left.PropertyTypeId, right => right.Id) - .InnerJoin().On((left, right) => left.Id == right.Id) + .InnerJoin().On((left, right) => left.VersionId == right.Id) .Where(x => x.Alias == "umbracoFile") .Append(query); diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index eb38f7e36e..8d0d3df40f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -66,7 +66,7 @@ namespace Umbraco.Core.Persistence.Repositories var baseQuery = GetBaseQuery(false); // fixme why is this different from content/media?! - //check if the query is based on properties or not + // check if the query is based on properties or not var wheres = query.GetWhereClauses(); //this is a pretty rudimentary check but wil work, we just need to know if this query requires property @@ -155,16 +155,17 @@ namespace Umbraco.Core.Persistence.Repositories return Sql() .Select("DISTINCT(umbracoNode.id)") .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.ContentTypeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .LeftJoin().On(left => left.ContentTypeId, right => right.ContentTypeId) - .LeftJoin().On(left => left.DataTypeId, right => right.DataTypeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.ContentTypeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + + .LeftJoin().On(left => left.ContentTypeId, right => right.ContentTypeId) + .LeftJoin().On(left => left.DataTypeId, right => right.DataTypeId) .LeftJoin().On(x => x .Where((left, right) => left.PropertyTypeId == right.Id) - .Where((left, right) => left.Id == right.Id)) + .Where((left, right) => left.VersionId == right.Id)) .Where(x => x.NodeObjectType == NodeObjectTypeId); } @@ -172,21 +173,21 @@ namespace Umbraco.Core.Persistence.Repositories protected override IEnumerable GetDeleteClauses() { var list = new List - { - "DELETE FROM cmsTask WHERE nodeId = @id", - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", - "DELETE FROM umbracoRelation WHERE parentId = @id", - "DELETE FROM umbracoRelation WHERE childId = @id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM cmsMember2MemberGroup WHERE Member = @id", - "DELETE FROM cmsMember WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", - "DELETE FROM cmsContentXml WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", - "DELETE FROM umbracoNode WHERE id = @id" - }; + { + "DELETE FROM cmsTask WHERE nodeId = @id", + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", + "DELETE FROM umbracoRelation WHERE parentId = @id", + "DELETE FROM umbracoRelation WHERE childId = @id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM cmsMember2MemberGroup WHERE Member = @id", + "DELETE FROM cmsMember WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", + "DELETE FROM cmsContentXml WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", + "DELETE FROM umbracoNode WHERE id = @id" + }; return list; } diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 91f369b9ad..11d4603212 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories { @@ -432,20 +433,12 @@ namespace Umbraco.Core.Persistence.Repositories var propertiesWithTagSupport = new Dictionary(); var compositionPropertiesIndex = new Dictionary(); - // index version ids - var indexedVersionId = new Dictionary(); - foreach (var temp in temps) - if (temp.PublishedVersionId > 0) - indexedVersionId[temp.PublishedVersionId] = temp.VersionId; - // index PropertyDataDto per versionId for perfs // merge edited and published dtos var indexedPropertyDataDtos = new Dictionary>(); foreach (var dto in allPropertyDataDtos) { var versionId = dto.VersionId; - if (indexedVersionId.TryGetValue(versionId, out var id)) // map published -> version - versionId = id; if (indexedPropertyDataDtos.TryGetValue(versionId, out var list) == false) indexedPropertyDataDtos[versionId] = list = new List(); list.Add(dto); @@ -459,9 +452,16 @@ namespace Umbraco.Core.Persistence.Repositories compositionPropertiesIndex[temp.ContentType.Id] = compositionProperties = temp.ContentType.CompositionPropertyTypes.ToArray(); // map the list of PropertyDataDto to a list of Property - var properties = indexedPropertyDataDtos.TryGetValue(temp.VersionId, out var propertyDataDtos) - ? PropertyFactory.BuildEntities(compositionProperties, propertyDataDtos, temp.PublishedVersionId).ToList() - : new List(); + var propertyDataDtos = new List(); + if (indexedPropertyDataDtos.TryGetValue(temp.VersionId, out var propertyDataDtos1)) + { + propertyDataDtos.AddRange(propertyDataDtos1); + if (temp.VersionId == temp.PublishedVersionId) // dirty corner case + propertyDataDtos.AddRange(propertyDataDtos1.Select(x => x.Clone(-1))); + } + if (temp.VersionId != temp.PublishedVersionId && indexedPropertyDataDtos.TryGetValue(temp.PublishedVersionId, out var propertyDataDtos2)) + propertyDataDtos.AddRange(propertyDataDtos2); + var properties = PropertyFactory.BuildEntities(compositionProperties, propertyDataDtos, temp.PublishedVersionId).ToList(); // deal with tags Dictionary additionalData = null; @@ -502,256 +502,6 @@ namespace Umbraco.Core.Persistence.Repositories return result; } - // fixme - copied from 7.6... - /* - /// - /// A helper method for inheritors to get the paged results by query in a way that minimizes queries - /// - /// The type of the d. - /// The query. - /// Index of the page. - /// Size of the page. - /// The total records. - /// The tablename + column name for the SELECT statement fragment to return the node id from the query - /// A callback to create the default filter to be applied if there is one - /// A callback to process the query result - /// The order by column - /// The order direction. - /// Flag to indicate when ordering by system field - /// - /// orderBy - protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - Tuple nodeIdSelect, - Func, IEnumerable> processQuery, - string orderBy, - Direction orderDirection, - bool orderBySystemField, - Func> defaultFilter = null) - { - if (orderBy == null) throw new ArgumentNullException(nameof(orderBy)); - - if (query == null) query = QueryT; - var sqlIds = new SqlTranslator(GetBaseQuery(QueryType.Ids), query).Translate(); - var sqlMany = new SqlTranslator(GetBaseQuery(QueryType.Many), query).Translate(); - - // sort and filter - var prepSqlIds = PrepareSqlForPagedResults(sqlIds, filterSql, orderBy, orderDirection, orderBySystemField, table); - //var sqlNodeIdsWithSort = GetSortedSqlForPagedResults( - // GetFilteredSqlForPagedResults(sqlQueryIdsOnly, defaultFilter), - // orderDirection, orderBy, orderBySystemField, nodeIdSelect); - - // get a page of DTOs and the total count - var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlIds); - totalRecords = Convert.ToInt32(pagedResult.TotalItems); - //var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); - //totalRecords = Convert.ToInt32(pagedResult.TotalItems); - - - // need to check the actual items returned, not the 'totalRecords', that is because if you request a page number - // that doesn't actually have any data on it, the totalRecords will still indicate there are records but there are none in - // the pageResult, then the GetAll will actually return ALL records in the db. - if (pagedResult.Items.Any() == false) - return Enumerable.Empty(); - - //Create the inner paged query that was used above to get the paged result, we'll use that as the inner sub query - var args = prepSqlIds.Arguments; - Database.BuildPageQueries(pageIndex * pageSize, pageSize, prepSqlIds.SQL, ref args, out string unused, out string sqlPage); - - //We need to make this FULL query an inner join on the paged ID query - var splitQuery = sqlMany.SQL.Split(new[] { "WHERE " }, StringSplitOptions.None); - var fullQueryWithPagedInnerJoin = new Sql(splitQuery[0]) - .Append("INNER JOIN (") - //join the paged query with the paged query arguments - .Append(sqlPage, args) - .Append(") temp ") - .Append(string.Format("ON {0}.{1} = temp.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)) - //add the original where clause back with the original arguments - .Where(splitQuery[1], sqlIds.Arguments); - - //get sorted and filtered sql - var fullQuery = GetSortedSqlForPagedResults( - GetFilteredSqlForPagedResults(fullQueryWithPagedInnerJoin, defaultFilter), - orderDirection, orderBy, orderBySystemField, nodeIdSelect); - - return processQuery(fullQuery, new PagingSqlQuery(Database, sqlNodeIdsWithSort, pageIndex, pageSize)); - } - - protected IDictionary GetPropertyCollection(Sql sql, IReadOnlyCollection documentDefs) - { - return GetPropertyCollection(new PagingSqlQuery(sql), documentDefs); - } - - protected IDictionary GetPropertyCollection(PagingSqlQuery pagingSqlQuery, IReadOnlyCollection documentDefs) - { - if (documentDefs.Count == 0) return new Dictionary(); - - //initialize to the query passed in - var docSql = pagingSqlQuery.QuerySql; - - //we need to parse the original SQL statement and reduce the columns to just cmsContent.nodeId, cmsContentVersion.VersionId so that we can use - // the statement to go get the property data for all of the items by using an inner join - var parsedOriginalSql = "SELECT {0} " + docSql.SQL.Substring(docSql.SQL.IndexOf("FROM", StringComparison.Ordinal)); - - if (pagingSqlQuery.HasPaging) - { - //if this is a paged query, build the paged query with the custom column substitution, then re-assign - docSql = pagingSqlQuery.BuildPagedQuery("{0}"); - parsedOriginalSql = docSql.SQL; - } - else if (parsedOriginalSql.InvariantContains("ORDER BY ")) - { - //now remove everything from an Orderby clause and beyond if this is unpaged data - parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); - } - - //This retrieves all pre-values for all data types that are referenced for all property types - // that exist in the data set. - //Benchmarks show that eagerly loading these so that we can lazily read the property data - // below (with the use of Query intead of Fetch) go about 30% faster, so we'll eagerly load - // this now since we cannot execute another reader inside of reading the property data. - var preValsSql = new Sql(@"SELECT a.id, a.value, a.sortorder, a.alias, a.datatypeNodeId -FROM cmsDataTypePreValues a -WHERE EXISTS( - SELECT DISTINCT b.id as preValIdInner - FROM cmsDataTypePreValues b - INNER JOIN cmsPropertyType - ON b.datatypeNodeId = cmsPropertyType.dataTypeId - INNER JOIN - (" + string.Format(parsedOriginalSql, "uContent.contentTypeId") + @") as docData - ON cmsPropertyType.contentTypeId = docData.contentType - WHERE a.id = b.id)", docSql.Arguments); - - var allPreValues = Database.Fetch(preValsSql); - - //It's Important with the sort order here! We require this to be sorted by node id, - // this is required because this data set can be huge depending on the page size. Due - // to it's size we need to be smart about iterating over the property values to build - // the document. Before we used to use Linq to get the property data for a given content node - // and perform a Distinct() call. This kills performance because that would mean if we had 7000 nodes - // and on each iteration we will perform a lookup on potentially 100,000 property rows against the node - // id which turns out to be a crazy amount of iterations. Instead we know it's sorted by this value we'll - // keep an index stored of the rows being read so we never have to re-iterate the entire data set - // on each document iteration. - var propSql = new Sql(@"SELECT " + Constants.DatabaseSchema.Tables.PropertyData + ".* -FROM " + Constants.DatabaseSchema.Tables.PropertyData + " -INNER JOIN cmsPropertyType -ON " + Constants.DatabaseSchema.Tables.PropertyData + ".propertytypeid = cmsPropertyType.id -INNER JOIN - (" + string.Format(parsedOriginalSql, "uContent.nodeId, cmsContentVersion.VersionId") + @") as docData -ON " + Constants.DatabaseSchema.Tables.PropertyData + ".versionId = docData.VersionId AND " + Constants.DatabaseSchema.Tables.PropertyData + ".nodeId = docData.nodeId -ORDER BY nodeId, versionId, propertytypeid -", docSql.Arguments); - - //This does NOT fetch all data into memory in a list, this will read - // over the records as a data reader, this is much better for performance and memory, - // but it means that during the reading of this data set, nothing else can be read - // from SQL server otherwise we'll get an exception. - var allPropertyData = Database.Query(propSql); - - var result = new Dictionary(); - var propertiesWithTagSupport = new Dictionary(); - //used to track the resolved composition property types per content type so we don't have to re-resolve (ToArray) the list every time - var resolvedCompositionProperties = new Dictionary(); - - //keep track of the current property data item being enumerated - var propertyDataSetEnumerator = allPropertyData.GetEnumerator(); - var hasCurrent = false; // initially there is no enumerator.Current - - var comparer = new DocumentDefinitionComparer(SqlSyntax); - - try - { - //This must be sorted by node id because this is how we are sorting the query to lookup property types above, - // which allows us to more efficiently iterate over the large data set of property values - foreach (var def in documentDefs.OrderBy(x => x.Id).ThenBy(x => x.Version, comparer)) - { - // get the resolved properties from our local cache, or resolve them and put them in cache - PropertyType[] compositionProperties; - if (resolvedCompositionProperties.ContainsKey(def.Composition.Id)) - { - compositionProperties = resolvedCompositionProperties[def.Composition.Id]; - } - else - { - compositionProperties = def.Composition.CompositionPropertyTypes.ToArray(); - resolvedCompositionProperties[def.Composition.Id] = compositionProperties; - } - - // assemble the dtos for this def - // use the available enumerator.Current if any else move to next - var propertyDataDtos = new List(); - while (hasCurrent || propertyDataSetEnumerator.MoveNext()) - { - //Not checking null on VersionId because it can never be null - no idea why it's set to nullable - // ReSharper disable once PossibleInvalidOperationException - if (propertyDataSetEnumerator.Current.VersionId.Value == def.Version) - { - hasCurrent = false; // enumerator.Current is not available - propertyDataDtos.Add(propertyDataSetEnumerator.Current); - } - else - { - hasCurrent = true; // enumerator.Current is available for another def - break; // no more propertyDataDto for this def - } - } - - var properties = PropertyFactory.BuildEntity(propertyDataDtos, compositionProperties, def.CreateDate, def.VersionDate).ToArray(); - - foreach (var property in properties) - { - //NOTE: The benchmarks run with and without the following code show very little change so this is not a perf bottleneck - var editor = Current.PropertyEditors[property.PropertyType.PropertyEditorAlias]; - - var tagSupport = propertiesWithTagSupport.ContainsKey(property.PropertyType.PropertyEditorAlias) - ? propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] - : TagExtractor.GetAttribute(editor); - - if (tagSupport != null) - { - //add to local cache so we don't need to reflect next time for this property editor alias - propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] = tagSupport; - - //this property has tags, so we need to extract them and for that we need the prevals which we've already looked up - var preValData = allPreValues.Where(x => x.DataTypeNodeId == property.PropertyType.DataTypeDefinitionId) - .Distinct() - .ToArray(); - - var asDictionary = preValData.ToDictionary(x => x.Alias, x => new PreValue(x.Id, x.Value, x.SortOrder)); - - var preVals = new PreValueCollection(asDictionary); - - var contentPropData = new ContentPropertyData(property.Value, preVals, new Dictionary()); - - TagExtractor.SetPropertyTags(property, contentPropData, property.Value, tagSupport); - } - } - - if (result.ContainsKey(def.Version)) - { - var msg = string.Format("The query returned multiple property sets for document definition {0}, {1}, {2}", def.Id, def.Version, def.Composition.Name); - if (ThrowOnWarning) - { - throw new InvalidOperationException(msg); - } - else - { - Logger.Warn>(msg); - } - } - result[def.Version] = new PropertyCollection(properties); - } - } - finally - { - propertyDataSetEnumerator.Dispose(); - } - - return result; - - } - */ - protected virtual string GetDatabaseFieldNameForOrderBy(string orderBy) { // translate the supplied "order by" field, which were originally defined for in-memory @@ -761,13 +511,13 @@ ORDER BY nodeId, versionId, propertytypeid { case "VERSIONDATE": case "UPDATEDATE": - return GetDatabaseFieldNameForOrderBy("cmsContentVersion", "versionDate"); + return GetDatabaseFieldNameForOrderBy("uContentVersion", "versionDate"); case "CREATEDATE": return GetDatabaseFieldNameForOrderBy("umbracoNode", "createDate"); case "NAME": return GetDatabaseFieldNameForOrderBy("umbracoNode", "text"); case "PUBLISHED": - return GetDatabaseFieldNameForOrderBy("cmsDocument", "published"); + return GetDatabaseFieldNameForOrderBy("uDocument", "published"); case "OWNER": //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter return GetDatabaseFieldNameForOrderBy("umbracoNode", "nodeUser"); @@ -1110,7 +860,7 @@ ORDER BY nodeId, versionId, propertytypeid protected virtual string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) { var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.EnsureUniqueNodeName", tsql => tsql - .SelectAs(x => x.NodeId, "id").AndSelectAs(x => x.Text, "name") + .Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name")) .From() .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId"))); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 2ae84f1c9c..167f9dc0b8 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -64,6 +64,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax string FormatPrimaryKey(TableDefinition table); string GetQuotedValue(string value); string Format(ColumnDefinition column); + string Format(ColumnDefinition column, string tableName, out IEnumerable sqls); string Format(IndexDefinition index); string Format(ForeignKeyDefinition foreignKey); string FormatColumnRename(string tableName, string oldName, string newName); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index ca795c7bf7..f775def629 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -349,6 +349,64 @@ namespace Umbraco.Core.Persistence.SqlSyntax .Where(clause => string.IsNullOrEmpty(clause) == false)); } + public virtual string Format(ColumnDefinition column, string tableName, out IEnumerable sqls) + { + var sql = new StringBuilder(); + sql.Append(FormatString(column)); + sql.Append(" "); + sql.Append(FormatType(column)); + sql.Append(" "); + sql.Append("NULL"); // always nullable + sql.Append(" "); + sql.Append(FormatConstraint(column)); + sql.Append(" "); + sql.Append(FormatDefaultValue(column)); + sql.Append(" "); + sql.Append(FormatPrimaryKey(column)); + sql.Append(" "); + sql.Append(FormatIdentity(column)); + + var isNullable = column.IsNullable; + + //var constraint = FormatConstraint(column)?.TrimStart("CONSTRAINT "); + //var hasConstraint = !string.IsNullOrWhiteSpace(constraint); + + //var defaultValue = FormatDefaultValue(column); + //var hasDefaultValue = !string.IsNullOrWhiteSpace(defaultValue); + + if (isNullable /*&& !hasConstraint && !hasDefaultValue*/) + { + sqls = Enumerable.Empty(); + return sql.ToString(); + } + + var msql = new List(); + sqls = msql; + + var alterSql = new StringBuilder(); + alterSql.Append(FormatString(column)); + alterSql.Append(" "); + alterSql.Append(FormatType(column)); + alterSql.Append(" "); + alterSql.Append(FormatNullable(column)); + //alterSql.Append(" "); + //alterSql.Append(FormatPrimaryKey(column)); + //alterSql.Append(" "); + //alterSql.Append(FormatIdentity(column)); + msql.Add(string.Format(AlterColumn, tableName, alterSql)); + + //if (hasConstraint) + //{ + // var dropConstraintSql = string.Format(DeleteConstraint, tableName, constraint); + // msql.Add(dropConstraintSql); + // var constraintType = hasDefaultValue ? defaultValue : ""; + // var createConstraintSql = string.Format(CreateConstraint, tableName, constraint, constraintType, FormatString(column)); + // msql.Add(createConstraintSql); + //} + + return sql.ToString(); + } + public virtual string FormatPrimaryKey(TableDefinition table) { var columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs index e5dcd89e7e..5f71cdbf40 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs @@ -326,7 +326,7 @@ namespace Umbraco.Core.PropertyEditors foreach (var pvalue in property.Values) { - var value = published ? pvalue.PublishedValue : pvalue.EditValue; + var value = published ? pvalue.PublishedValue : pvalue.EditedValue; if (value == null || value is string stringValue && string.IsNullOrWhiteSpace(stringValue)) continue; diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index eae7795250..b9b873bd00 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1844,13 +1844,27 @@ namespace Umbraco.Core.Services return origContent; } - ((Content) currContent).RollbackAllValues(origContent); + // orig content versions + // pk < published pk = normal, rollback to an older version + // pk = published pk = normal, rollback to currently published version + // pk > published pk = special, rollback to current 'edit' version + // + // in that last case, we want to copy the published values + var copyPublished = ((Content) origContent).VersionPk > ((Content) origContent).PublishedVersionPk; + ((Content) currContent).CopyAllValues(origContent, copyPublished); currContent.WriterId = userId; - // publish name is always the current publish name - // but name is the actual version name, this is what we want - currContent.Name = origContent.Name; - currContent.Template = origContent.Template; + // builtin values + if (copyPublished) + { + currContent.Name = origContent.PublishName; + currContent.Template = origContent.PublishTemplate; + } + else + { + currContent.Name = origContent.Name; + currContent.Template = origContent.Template; + } // save the values repository.AddOrUpdate(currContent); diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index aabfa83e4e..486f4f1424 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -518,7 +518,7 @@ namespace Umbraco.Core.Services public virtual IEnumerable GetAll(params int[] ids) where T : IUmbracoEntity { var typeFullName = typeof(T).FullName; - if (_supportedObjectTypes.ContainsKey(typeFullName) == false) + if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) throw new NotSupportedException("The passed in type is not supported"); var objectType = _supportedObjectTypes[typeFullName].Item1; @@ -536,7 +536,7 @@ namespace Umbraco.Core.Services var entityType = GetEntityType(umbracoObjectType); var typeFullName = entityType.FullName; - if (_supportedObjectTypes.ContainsKey(typeFullName) == false) + if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) throw new NotSupportedException("The passed in type is not supported"); var objectTypeId = umbracoObjectType.GetGuid(); @@ -552,7 +552,7 @@ namespace Umbraco.Core.Services var entityType = GetEntityType(umbracoObjectType); var typeFullName = entityType.FullName; - if (_supportedObjectTypes.ContainsKey(typeFullName) == false) + if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) throw new NotSupportedException("The passed in type is not supported"); var objectTypeId = umbracoObjectType.GetGuid(); @@ -567,7 +567,7 @@ namespace Umbraco.Core.Services { var entityType = GetEntityType(umbracoObjectType); var typeFullName = entityType.FullName; - if (_supportedObjectTypes.ContainsKey(typeFullName) == false) + if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) throw new NotSupportedException("The passed in type is not supported."); var objectTypeId = umbracoObjectType.GetGuid(); @@ -582,7 +582,7 @@ namespace Umbraco.Core.Services { var entityType = GetEntityType(umbracoObjectType); var typeFullName = entityType.FullName; - if (_supportedObjectTypes.ContainsKey(typeFullName) == false) + if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) throw new NotSupportedException("The passed in type is not supported."); var objectTypeId = umbracoObjectType.GetGuid(); @@ -605,7 +605,7 @@ namespace Umbraco.Core.Services var entityType = GetEntityType(umbracoObjectType); var typeFullName = entityType.FullName; - if (_supportedObjectTypes.ContainsKey(typeFullName) == false) + if (typeFullName == null || _supportedObjectTypes.ContainsKey(typeFullName) == false) throw new NotSupportedException("The passed in type is not supported"); using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) @@ -660,10 +660,9 @@ namespace Umbraco.Core.Services /// public virtual UmbracoObjectTypes GetObjectType(IUmbracoEntity entity) { - var entityImpl = entity as UmbracoEntity; - return entityImpl == null - ? GetObjectType(entity.Id) - : UmbracoObjectTypesExtensions.GetUmbracoObjectType(entityImpl.NodeObjectTypeId); + return entity is UmbracoEntity entityImpl + ? UmbracoObjectTypesExtensions.GetUmbracoObjectType(entityImpl.NodeObjectTypeId) + : GetObjectType(entity.Id); } /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index d0dc573b31..9700b2f9b3 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -828,7 +828,6 @@ - diff --git a/src/Umbraco.Tests/Integration/ContentEventsTests.cs b/src/Umbraco.Tests/Integration/ContentEventsTests.cs index c732a8b0fe..95db52f881 100644 --- a/src/Umbraco.Tests/Integration/ContentEventsTests.cs +++ b/src/Umbraco.Tests/Integration/ContentEventsTests.cs @@ -208,34 +208,25 @@ namespace Umbraco.Tests.Integration Name = "Refresh", Args = string.Join(",", entities.Select(x => { - // saved content - var state = (x.Published ? "p" : "u"); + var publishedState = ((Content) x).PublishedState; - // published version - if (x.Published == false) - { - // x is not a published version - - // unpublishing content, clear - // handlers would probably clear data - if (((Content)x).PublishedState == PublishedState.Unpublishing) - { - state += "x"; - } - else if (x.Published) - { - var isPathPublished = ((ContentRepository)sender).IsPathPublished(x); // expensive! - if (isPathPublished == false) - state += "m"; // masked - else if (HasChangesImpactingAllVersions(x)) - state += "p"; // refresh (using published = sender.GetByVersion(x.PublishedVersionGuid)) - else - state += "u"; // no impact on published version - } - else - state += "u"; // no published version - } + var xstate = x.Published ? "p" : "u"; + if (publishedState == PublishedState.Publishing) + xstate += "+" + (sender.IsPathPublished(x) ? "p" : "m"); + else if (publishedState == PublishedState.Unpublishing) + xstate += "-u"; else + xstate += "=" + (x.Published ? (sender.IsPathPublished(x) ? "p" : "m") : "u"); + + return $"{xstate}-{x.Id}"; + + + var willBePublished = publishedState == PublishedState.Publishing || x.Published; // && ((Content) x).PublishedState == PublishedState.Unpublishing; + + // saved content + var state = willBePublished ? "p" : "u"; + + if (publishedState == PublishedState.Publishing) { // content is published and x is the published version // figure out whether it is masked or not - what to do exactly in each case @@ -247,6 +238,63 @@ namespace Umbraco.Tests.Integration else state += "m"; // masked } + else if (publishedState == PublishedState.Unpublishing) + { + // unpublishing content, clear + // handlers would probably clear data + state += "x"; + } + else if (publishedState == PublishedState.Published) + { + var isPathPublished = sender.IsPathPublished(x); // expensive! + if (isPathPublished == false) + state += "m"; // masked + else if (HasChangesImpactingAllVersions(x)) + state += "p"; // refresh (using published = sender.GetByVersion(x.PublishedVersionGuid)) + else + state += "u"; // no impact on published version + } + else // Unpublished + { + state += "u"; // no published version + } + + //// published version + //if (x.Published == false) + //{ + // // x is not a published version + + // // unpublishing content, clear + // // handlers would probably clear data + // if (((Content)x).PublishedState == PublishedState.Unpublishing) + // { + // state += "x"; + // } + // else if (x.Published) + // { + // var isPathPublished = sender.IsPathPublished(x); // expensive! + // if (isPathPublished == false) + // state += "m"; // masked + // else if (HasChangesImpactingAllVersions(x)) + // state += "p"; // refresh (using published = sender.GetByVersion(x.PublishedVersionGuid)) + // else + // state += "u"; // no impact on published version + // } + // else + // state += "u"; // no published version + //} + //else + //{ + // // content is published and x is the published version + // // figure out whether it is masked or not - what to do exactly in each case + // // would depend on the handler implementation - ie is it still updating + // // data for masked version or not + // var isPathPublished = sender.IsPathPublished(x); // expensive! + // if (isPathPublished) + // state += "p"; // refresh (using x) + // else + // state += "m"; // masked + //} return $"{state}-{x.Id}"; })) @@ -354,7 +402,7 @@ namespace Umbraco.Tests.Integration Assert.AreEqual(2, _events.Count); var i = 0; var m = 0; - Assert.AreEqual(string.Format("{0:000}: ContentRepository/Refresh/uu-{1}", m, content.Id), _events[i++].ToString()); + Assert.AreEqual(string.Format("{0:000}: ContentRepository/Refresh/u=u-{1}", m, content.Id), _events[i++].ToString()); m++; Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshNode/{1}", m, content.Id), _events[i].ToString()); } @@ -378,7 +426,7 @@ namespace Umbraco.Tests.Integration Assert.AreEqual(2, _events.Count); var i = 0; var m = 0; - Assert.AreEqual(string.Format("{0:000}: ContentRepository/Refresh/uu-{1}", m, content.Id), _events[i++].ToString()); + Assert.AreEqual(string.Format("{0:000}: ContentRepository/Refresh/u=u-{1}", m, content.Id), _events[i++].ToString()); m++; Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshNode/{1}", m, content.Id), _events[i].ToString()); @@ -390,7 +438,7 @@ namespace Umbraco.Tests.Integration Assert.AreEqual(2, _events.Count); i = 0; m = 0; - Assert.AreEqual(string.Format("{0:000}: ContentRepository/Refresh/uu-{1}", m, content.Id), _events[i++].ToString()); + Assert.AreEqual(string.Format("{0:000}: ContentRepository/Refresh/u=u-{1}", m, content.Id), _events[i++].ToString()); m++; Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshNode/{1}", m, content.Id), _events[i].ToString()); } @@ -414,7 +462,7 @@ namespace Umbraco.Tests.Integration Assert.AreEqual(2, _events.Count); var i = 0; var m = 0; - Assert.AreEqual(string.Format("{0:000}: ContentRepository/Refresh/up-{1}", m, content.Id), _events[i++].ToString()); + Assert.AreEqual(string.Format("{0:000}: ContentRepository/Refresh/p=p-{1}", m, content.Id), _events[i++].ToString()); m++; Assert.AreEqual(string.Format("{0:000}: ContentCacheRefresher/RefreshNode/{1}", m, content.Id), _events[i].ToString()); diff --git a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs index 2fd872a9bb..71c2a03295 100644 --- a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs +++ b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlExtensionsTests.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; using Umbraco.Tests.TestHelpers; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Tests.Persistence.NPocoTests { @@ -88,6 +89,28 @@ FROM [dto1] INNER JOIN [dto2] ON [dto1].[id] = [dto2].[dto1id]".NoCrLf(), sql.SQL.NoCrLf()); } + [Test] + public void SelectAliasTests() + { + // and select - not good + var sql = Sql() + .Select(x => x.Id) + .Select(x => x.Id); + Assert.AreEqual("SELECT [dto1].[id] AS [Id] SELECT [dto2].[id] AS [Id]".NoCrLf(), sql.SQL.NoCrLf()); + + // and select - good + sql = Sql() + .Select(x => x.Id) + .AndSelect(x => x.Id); + Assert.AreEqual("SELECT [dto1].[id] AS [Id] , [dto2].[id] AS [Id]".NoCrLf(), sql.SQL.NoCrLf()); + + // and select + alias + sql = Sql() + .Select(x => x.Id) + .AndSelect(x => Alias(x.Id, "id2")); + Assert.AreEqual("SELECT [dto1].[id] AS [Id] , [dto2].[id] AS [id2]".NoCrLf(), sql.SQL.NoCrLf()); + } + [TableName("dto1")] [PrimaryKey("id", AutoIncrement = false)] [ExplicitColumns] diff --git a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTests.cs b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTests.cs index 599804fc2f..38db58c7fb 100644 --- a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTests.cs +++ b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTests.cs @@ -174,6 +174,30 @@ namespace Umbraco.Tests.Persistence.NPocoTests Assert.AreEqual(3, sql.Arguments[1]); } + [Test] + public void Where_Null() + { + var sql = Sql().SelectAll().From().WhereNull(x => x.NodeId); + Assert.AreEqual("SELECT * FROM [umbracoNode] WHERE (([umbracoNode].[id] IS NULL))", sql.SQL.Replace("\n", " ")); + } + + [Test] + public void Where_Not_Null() + { + var sql = Sql().SelectAll().From().WhereNotNull(x => x.NodeId); + Assert.AreEqual("SELECT * FROM [umbracoNode] WHERE (([umbracoNode].[id] IS NOT NULL))", sql.SQL.Replace("\n", " ")); + } + + [Test] + public void Where_Any() + { + var sql = Sql().SelectAll().From().WhereAny( + s => s.Where(x => x.NodeId == 1), + s => s.Where(x => x.NodeId == 2)); + + Assert.AreEqual("SELECT * FROM [umbracoNode] WHERE (( (([umbracoNode].[id] = @0)) ) OR ( (([umbracoNode].[id] = @1)) ))", sql.SQL.Replace("\n", " ")); + } + [Test] public void Can_Select_From_With_Type() { diff --git a/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs b/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs index f3263b711f..3812c87063 100644 --- a/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs +++ b/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs @@ -208,5 +208,66 @@ namespace Umbraco.Tests.Persistence.Querying Assert.AreEqual("hello@world.com", modelToSqlExpressionHelper.GetSqlParameters()[1]); Assert.AreEqual("blah@blah.com", modelToSqlExpressionHelper.GetSqlParameters()[2]); } + + private string GetSomeValue(string s) + { + return "xx" + s + "xx"; + } + + private class Foo + { + public string Value { get; set; } + } + + [Test] + public void StartsWith() + { + Expression> predicate = user => user.Login.StartsWith("aaaaa"); + var modelToSqlExpressionHelper = new PocoToSqlExpressionVisitor(SqlContext, null); + var result = modelToSqlExpressionHelper.Visit(predicate); + + Console.WriteLine(result); + Assert.AreEqual("upper([umbracoUser].[userLogin]) LIKE upper(@0)", result); + + predicate = user => user.Login.StartsWith(GetSomeValue("aaaaa")); + modelToSqlExpressionHelper = new PocoToSqlExpressionVisitor(SqlContext, null); + result = modelToSqlExpressionHelper.Visit(predicate); + + Console.WriteLine(result); + Assert.AreEqual("upper([umbracoUser].[userLogin]) LIKE upper(@0)", result); + + predicate = user => user.Login.StartsWith(GetSomeValue("aaaaa")); + modelToSqlExpressionHelper = new PocoToSqlExpressionVisitor(SqlContext, null); + result = modelToSqlExpressionHelper.Visit(predicate); + + Console.WriteLine(result); + Assert.AreEqual("upper([umbracoUser].[userLogin]) LIKE upper(@0)", result); + + var foo = new Foo { Value = "aaaaa" }; + predicate = user => user.Login.StartsWith(foo.Value); + modelToSqlExpressionHelper = new PocoToSqlExpressionVisitor(SqlContext, null); + result = modelToSqlExpressionHelper.Visit(predicate); + + Console.WriteLine(result); + Assert.AreEqual("upper([umbracoUser].[userLogin]) LIKE upper(@0)", result); + + // below does not work, we want to output + // LIKE concat([group].[name], ',%') + // and how would we get the comma there? we'd have to parse .StartsWith(group.Name + ',') + // which is going to be quite complicated => no + + //// how can we do user.Login.StartsWith(other.Value) ?? to use in WHERE or JOIN.ON clauses ?? + //Expression> predicate2 = (user, group) => user.Login.StartsWith(group.Name); + //var modelToSqlExpressionHelper2 = new PocoToSqlExpressionVisitor(SqlContext, null, null); + //var result2 = modelToSqlExpressionHelper2.Visit(predicate2); // fails, for now + + //Console.WriteLine(result2); + + Expression> predicate3 = (user, group) => NPocoSqlExtensions.Statics.SqlText(user.Login, group.Name, (n1, n2) => $"({n1} LIKE concat({n2}, ',%'))"); + var modelToSqlExpressionHelper3 = new PocoToSqlExpressionVisitor(SqlContext, null, null); + var result3 = modelToSqlExpressionHelper3.Visit(predicate3); + + Console.WriteLine(result3); + } } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index f533ba61b7..6cee65363c 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -139,16 +139,16 @@ namespace Umbraco.Tests.Persistence.Repositories unitOfWork.Flush(); versions.Add(content1.Version); // the first version - // publish = no new version (was not published) + // publish = new edit version content1.SetValue("title", "title"); ((Content) content1).PublishValues(); ((Content) content1).PublishedState = PublishedState.Publishing; repository.AddOrUpdate(content1); unitOfWork.Flush(); - versions.Add(content1.Version); // the same version + versions.Add(content1.Version); // NEW VERSION - // no new version has been created - Assert.AreEqual(versions[versions.Count - 2], versions[versions.Count - 1]); + // new edit version has been created + Assert.AreNotEqual(versions[versions.Count - 2], versions[versions.Count - 1]); Assert.IsTrue(content1.Published); Assert.AreEqual(PublishedState.Published, ((Content) content1).PublishedState); Assert.AreEqual(versions[versions.Count - 1], repository.Get(content1.Id).Version); @@ -174,14 +174,14 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(true, unitOfWork.Database.ExecuteScalar("SELECT published FROM uDocument WHERE nodeId=@id", new { id = content1.Id })); Console.WriteLine(unitOfWork.Database.ExecuteScalar("SELECT updateDate FROM uContent WHERE nodeId=@id", new { id = content1.Id })); - // unpublish = should create a new version + // unpublish = no impact on versions ((Content) content1).PublishedState = PublishedState.Unpublishing; repository.AddOrUpdate(content1); unitOfWork.Flush(); - versions.Add(content1.Version); // the new version + versions.Add(content1.Version); // the same version - // a new version has been created - Assert.AreNotEqual(versions[versions.Count - 2], versions[versions.Count - 1]); + // no new version has been created + Assert.AreEqual(versions[versions.Count - 2], versions[versions.Count - 1]); Assert.IsFalse(content1.Published); Assert.AreEqual(PublishedState.Unpublished, ((Content) content1).PublishedState); Assert.AreEqual(versions[versions.Count - 1], repository.Get(content1.Id).Version); @@ -206,15 +206,15 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(false, unitOfWork.Database.ExecuteScalar("SELECT published FROM uDocument WHERE nodeId=@id", new { id = content1.Id })); Console.WriteLine(unitOfWork.Database.ExecuteScalar("SELECT updateDate FROM uContent WHERE nodeId=@id", new { id = content1.Id })); - // publish = no new version (was not published) + // publish = version ((Content) content1).PublishValues(); ((Content) content1).PublishedState = PublishedState.Publishing; repository.AddOrUpdate(content1); unitOfWork.Flush(); - versions.Add(content1.Version); // the same version + versions.Add(content1.Version); // NEW VERSION - // no new version has been created - Assert.AreEqual(versions[versions.Count - 2], versions[versions.Count - 1]); + // new version has been created + Assert.AreNotEqual(versions[versions.Count - 2], versions[versions.Count - 1]); Assert.IsTrue(content1.Published); Assert.AreEqual(PublishedState.Published, ((Content) content1).PublishedState); Assert.AreEqual(versions[versions.Count - 1], repository.Get(content1.Id).Version); @@ -227,7 +227,9 @@ namespace Umbraco.Tests.Persistence.Repositories // save = update the current (draft) version content1.Name = "name-3"; content1.SetValue("title", "title-3"); + //Thread.Sleep(2000); // force date change + repository.AddOrUpdate(content1); unitOfWork.Flush(); versions.Add(content1.Version); // the same version @@ -240,14 +242,14 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(true, unitOfWork.Database.ExecuteScalar("SELECT published FROM uDocument WHERE nodeId=@id", new { id = content1.Id })); Console.WriteLine(unitOfWork.Database.ExecuteScalar("SELECT updateDate FROM uContent WHERE nodeId=@id", new { id = content1.Id })); - // publish = new version (was published) + // publish = new version content1.Name = "name-4"; content1.SetValue("title", "title-4"); ((Content) content1).PublishValues(); ((Content) content1).PublishedState = PublishedState.Publishing; repository.AddOrUpdate(content1); unitOfWork.Flush(); - versions.Add(content1.Version); // new version + versions.Add(content1.Version); // NEW VERSION // a new version has been created Assert.AreNotEqual(versions[versions.Count - 2], versions[versions.Count - 1]); @@ -259,17 +261,29 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(true, unitOfWork.Database.ExecuteScalar("SELECT published FROM uDocument WHERE nodeId=@id", new { id = content1.Id })); Console.WriteLine(unitOfWork.Database.ExecuteScalar("SELECT updateDate FROM uContent WHERE nodeId=@id", new { id = content1.Id })); + // all versions + var allVersions = repository.GetAllVersions(content1.Id).ToArray(); + Console.WriteLine(); + foreach (var v in versions) + Console.WriteLine(v); + Console.WriteLine(); + foreach (var v in allVersions) + { + var c = (Content) v; + Console.WriteLine($"{c.Id} {c.Version} {(c.Published ? "+" : "-")}pub pk={c.VersionPk} ppk={c.PublishedVersionPk} name=\"{c.Name}\" pname=\"{c.PublishName}\""); + } + // get older version - var content = repository.GetByVersion(versions[versions.Count - 2]); + var content = repository.GetByVersion(versions[versions.Count - 4]); Assert.IsNotNull(content.Version); - Assert.AreEqual(versions[versions.Count - 2], content.Version); + Assert.AreEqual(versions[versions.Count - 4], content.Version); Assert.AreEqual("name-4", content1.Name); Assert.AreEqual("title-4", content1.GetValue("title")); - Assert.AreEqual("name-3", content.Name); - Assert.AreEqual("title-3", content.GetValue("title")); + Assert.AreEqual("name-2", content.Name); + Assert.AreEqual("title-2", content.GetValue("title")); // get all versions - most recent first - var allVersions = repository.GetAllVersions(content1.Id).ToArray(); + allVersions = repository.GetAllVersions(content1.Id).ToArray(); var expVersions = versions.Distinct().Reverse().ToArray(); Assert.AreEqual(expVersions.Length, allVersions.Length); for (var i = 0; i < expVersions.Length; i++) diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index b83294255d..8231f5cd08 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -322,8 +322,8 @@ namespace Umbraco.Tests.Persistence.Repositories } var sql = provider.SqlContext.Sql(); - sql.Select("umbracoNode.*", "uContent.contentTypeId", "cmsContentType.alias AS ContentTypeAlias", "cmsContentVersion.VersionId", - "cmsContentVersion.VersionDate", "cmsMember.Email", + sql.Select("umbracoNode.*", "uContent.contentTypeId", "cmsContentType.alias AS ContentTypeAlias", "uContentVersion.versionId", + "uContentVersion.versionDate", "cmsMember.Email", "cmsMember.LoginName", "cmsMember.Password", Constants.DatabaseSchema.Tables.PropertyData + ".id AS PropertyDataId", Constants.DatabaseSchema.Tables.PropertyData + ".propertytypeid", Constants.DatabaseSchema.Tables.PropertyData + ".dateValue", Constants.DatabaseSchema.Tables.PropertyData + ".intValue", @@ -340,7 +340,7 @@ namespace Umbraco.Tests.Persistence.Repositories .LeftJoin().On(left => left.ContentTypeId, right => right.ContentTypeId) .LeftJoin().On(left => left.DataTypeId, right => right.DataTypeId) .LeftJoin().On(left => left.PropertyTypeId, right => right.Id) - .Append("AND " + Constants.DatabaseSchema.Tables.PropertyData + ".versionId = cmsContentVersion.VersionId") + .Append("AND " + Constants.DatabaseSchema.Tables.PropertyData + ".versionId = uContentVersion.id") .Where(x => x.NodeObjectType == NodeObjectTypeId); return sql; } @@ -358,7 +358,7 @@ namespace Umbraco.Tests.Persistence.Repositories .LeftJoin().On(left => left.ContentTypeId, right => right.ContentTypeId) .LeftJoin().On(left => left.DataTypeId, right => right.DataTypeId) .LeftJoin().On(left => left.PropertyTypeId, right => right.Id) - .Append("AND " + Constants.DatabaseSchema.Tables.PropertyData + ".versionId = cmsContentVersion.VersionId") + .Append("AND " + Constants.DatabaseSchema.Tables.PropertyData + ".versionId = uContentVersion.id") .Where(x => x.NodeObjectType == NodeObjectTypeId); return sql; } diff --git a/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs b/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs index 3f8e7b3b9a..fee20afc18 100644 --- a/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs +++ b/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs @@ -190,6 +190,7 @@ namespace Umbraco.Tests.Publishing ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); // else, FK violation on contentType! ServiceContext.ContentTypeService.Save(contentType); var mandatoryType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); + //ServiceContext.FileService.SaveTemplate(mandatoryType.DefaultTemplate); // else, FK violation on contentType! ServiceContext.ContentTypeService.Save(mandatoryType); //Create and Save Content "Homepage" based on "umbTextpage" -> 1046 diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 6843666150..2228ab8c1a 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -252,7 +253,7 @@ namespace Umbraco.Tests.Services // Assert var allVersions = contentService.GetVersionIds(content.Id, int.MaxValue); - Assert.AreEqual(20, allVersions.Count()); + Assert.AreEqual(21, allVersions.Count()); var topVersions = contentService.GetVersionIds(content.Id, 4); Assert.AreEqual(4, topVersions.Count()); @@ -1036,27 +1037,60 @@ namespace Umbraco.Tests.Services Assert.AreEqual(1, versions.Count); var version1 = content.Version; + Console.WriteLine($"1 e={((Content)content).VersionPk} p={((Content)content).PublishedVersionPk}"); content.Name = "Text Page 2 Updated"; content.SetValue("author", "Jane Doe"); - contentService.SaveAndPublishWithStatus(content); // publishes the current version + contentService.SaveAndPublishWithStatus(content); // publishes the current version, creates a version + + var version2 = content.Version; + Console.WriteLine($"2 e={((Content) content).VersionPk} p={((Content) content).PublishedVersionPk}"); content.Name = "Text Page 2 ReUpdated"; content.SetValue("author", "Bob Hope"); contentService.SaveAndPublishWithStatus(content); // publishes again, creates a version - var version2 = content.Version; + var version3 = content.Version; + Console.WriteLine($"3 e={((Content) content).VersionPk} p={((Content) content).PublishedVersionPk}"); + + var content1 = contentService.GetById(content.Id); + Assert.AreEqual("Bob Hope", content1.GetValue("author")); + Assert.AreEqual("Bob Hope", content1.GetValue("author", published: true)); + + content.Name = "Text Page 2 ReReUpdated"; + content.SetValue("author", "John Farr"); + contentService.Save(content); // no new version + + content1 = contentService.GetById(content.Id); + Assert.AreEqual("John Farr", content1.GetValue("author")); + Assert.AreEqual("Bob Hope", content1.GetValue("author", published: true)); versions = contentService.GetVersions(NodeDto.NodeIdSeed + 4).ToList(); - Assert.AreEqual(2, versions.Count); + Assert.AreEqual(3, versions.Count); // versions come with most recent first - Assert.AreEqual(version2, versions[0].Version); - Assert.AreEqual(version1, versions[1].Version); + Assert.AreEqual(version3, versions[0].Version); // the edited version + Assert.AreEqual(version2, versions[1].Version); // the published version + Assert.AreEqual(version1, versions[2].Version); // the previously published version + + // p is always the same, published version + // e is changing, actual version we're loading + Console.WriteLine(); + foreach (var version in ((IEnumerable) versions).Reverse()) + Console.WriteLine($"+ e={((Content) version).VersionPk} p={((Content) version).PublishedVersionPk}"); // and proper values - Assert.AreEqual("Bob Hope", versions[0].GetValue("author")); - Assert.AreEqual("Jane Doe", versions[1].GetValue("author", published: true)); + // first, the current (edited) version, with edited and published versions + Assert.AreEqual("John Farr", versions[0].GetValue("author")); // current version has the edited value + Assert.AreEqual("Bob Hope", versions[0].GetValue("author", published: true)); // and the published published value + + // then, the current (published) version, with edited == published + Assert.AreEqual("Bob Hope", versions[1].GetValue("author")); // own edited version + Assert.AreEqual("Bob Hope", versions[1].GetValue("author", published: true)); // and published + + // then, the first published version - with values as 'edited' + Assert.AreEqual("Jane Doe", versions[2].GetValue("author")); // own edited version + Assert.AreEqual("Bob Hope", versions[2].GetValue("author", published: true)); // and published } [Test] @@ -1872,23 +1906,25 @@ namespace Umbraco.Tests.Services var versions = contentService.GetVersions(NodeDto.NodeIdSeed + 4).ToList(); Assert.AreEqual(1, versions.Count); + var version1 = content.Version; + content.Name = "Text Page 2 Updated"; content.SetValue("author", "Francis Doe"); // non published = edited Assert.IsTrue(content.Edited); - contentService.SaveAndPublishWithStatus(content); // current version becomes 'published' + contentService.SaveAndPublishWithStatus(content); // new version + var version2 = content.Version; + Assert.AreNotEqual(version1, version2); Assert.IsTrue(content.Published); Assert.IsFalse(content.Edited); - Assert.AreEqual("Francis Doe", contentService.GetById(content.Id).GetValue("author")); // version1 author is Francis + Assert.AreEqual("Francis Doe", contentService.GetById(content.Id).GetValue("author")); // version2 author is Francis Assert.AreEqual("Text Page 2 Updated", content.Name); Assert.AreEqual("Text Page 2 Updated", content.PublishName); - var version1 = content.Version; - content.Name = "Text Page 2 ReUpdated"; content.SetValue("author", "Jane Doe"); @@ -1902,17 +1938,21 @@ namespace Umbraco.Tests.Services content.Name = "Text Page 2 ReReUpdated"; - contentService.SaveAndPublishWithStatus(content); // saves a version + contentService.SaveAndPublishWithStatus(content); // new version + var version3 = content.Version; + Assert.AreNotEqual(version2, version3); Assert.IsTrue(content.Published); Assert.IsFalse(content.Edited); - Assert.AreEqual("Jane Doe", contentService.GetById(content.Id).GetValue("author")); // version2 author is Jane + Assert.AreEqual("Jane Doe", contentService.GetById(content.Id).GetValue("author")); // version3 author is Jane Assert.AreEqual("Text Page 2 ReReUpdated", content.Name); Assert.AreEqual("Text Page 2 ReReUpdated", content.PublishName); - var version2 = content.Version; - Assert.AreNotEqual(version1, version2); + // here we have + // version1, first published version + // version2, second published version + // version3, third and current published version // rollback all values to version1 var rollback = contentService.Rollback(NodeDto.NodeIdSeed + 4, version1); @@ -1920,30 +1960,41 @@ namespace Umbraco.Tests.Services Assert.IsNotNull(rollback); Assert.IsTrue(rollback.Published); Assert.IsTrue(rollback.Edited); - Assert.AreEqual("Francis Doe", contentService.GetById(content.Id).GetValue("author")); // version2 author is now Francis again + Assert.AreEqual("Francis Doe", contentService.GetById(content.Id).GetValue("author")); // author is now Francis again + Assert.AreEqual(version3, rollback.Version); // same version but with edits - Assert.AreEqual(version2, rollback.Version); // same version but with edits + // props and name have rolled back Assert.AreEqual("Francis Doe", rollback.GetValue("author")); - Assert.AreEqual("Text Page 2 Updated", rollback.Name); // and the name has rolled back too + Assert.AreEqual("Text Page 2 Updated", rollback.Name); - // can rollback to current too (clears changes) - var rollback2 = contentService.Rollback(NodeDto.NodeIdSeed + 4, version2); + // published props and name are still there + Assert.AreEqual("Jane Doe", rollback.GetValue("author", true)); + Assert.AreEqual("Text Page 2 ReReUpdated", rollback.PublishName); - Assert.IsTrue(rollback2.Published); // because current was published + // rollback all values to current version + // special because... current has edits... this really is equivalent to rolling back to version2 + var rollback2 = contentService.Rollback(NodeDto.NodeIdSeed + 4, version3); + + Assert.IsTrue(rollback2.Published); Assert.IsFalse(rollback2.Edited); // all changes cleared! - // fixme - also test name here + Assert.AreEqual("Jane Doe", rollback2.GetValue("author")); + Assert.AreEqual("Text Page 2 ReReUpdated", rollback2.Name); + // test rollback to self, again content = contentService.GetById(content.Id); - Assert.AreEqual("Jane Doe", content.GetValue("author")); // Francis was not published, so back to Jane + Assert.AreEqual("Text Page 2 ReReUpdated", content.Name); + Assert.AreEqual("Jane Doe", content.GetValue("author")); contentService.SaveAndPublishWithStatus(content); Assert.IsFalse(content.Edited); - content.SetValue("author", "Bob Doe"); // changed to Bob + content.Name = "Xxx"; + content.SetValue("author", "Bob Doe"); contentService.Save(content); Assert.IsTrue(content.Edited); content = contentService.Rollback(content.Id, content.Version); Assert.IsFalse(content.Edited); - Assert.AreEqual("Jane Doe", content.GetValue("author")); // back to Jane + Assert.AreEqual("Text Page 2 ReReUpdated", content.Name); + Assert.AreEqual("Jane Doe", content.GetValue("author")); } [Test] diff --git a/src/Umbraco.Tests/Services/EntityServiceTests.cs b/src/Umbraco.Tests/Services/EntityServiceTests.cs index 8aa9e1c68a..ec51a8576f 100644 --- a/src/Umbraco.Tests/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests/Services/EntityServiceTests.cs @@ -503,13 +503,19 @@ namespace Umbraco.Tests.Services var entities = service.GetAll(UmbracoObjectTypes.Media).ToArray(); Assert.That(entities.Any(), Is.True); - Assert.That(entities.Count(), Is.EqualTo(5)); + Assert.That(entities.Length, Is.EqualTo(5)); - Assert.That( - entities.Any( - x => - x.AdditionalData.Any(y => y.Value is UmbracoEntity.EntityProperty - && ((UmbracoEntity.EntityProperty)y.Value).PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias)), Is.True); + foreach (var entity in entities) + { + Console.WriteLine(); + foreach (var data in entity.AdditionalData) + { + Console.WriteLine($"{entity.Id} {data.Key} {data.Value} {(data.Value is UmbracoEntity.EntityProperty p ? p.PropertyEditorAlias : "")}"); + } + } + + Assert.That(entities.Any(x => + x.AdditionalData.Any(y => y.Value is UmbracoEntity.EntityProperty && ((UmbracoEntity.EntityProperty) y.Value).PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias)), Is.True); } [Test] diff --git a/src/Umbraco.Tests/Services/MemberServiceTests.cs b/src/Umbraco.Tests/Services/MemberServiceTests.cs index e534a85a87..e1ad6e413e 100644 --- a/src/Umbraco.Tests/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests/Services/MemberServiceTests.cs @@ -1139,7 +1139,7 @@ namespace Umbraco.Tests.Services var sql = Current.SqlContext.Sql().Select() .From() .InnerJoin().On(dto => dto.PropertyTypeId, dto => dto.Id) - .InnerJoin().On((left, right) => left.Id == right.Id) + .InnerJoin().On((left, right) => left.VersionId == right.Id) .Where(dto => dto.NodeId == member.Id) .Where(dto => dto.Alias == Constants.Conventions.Member.LastLoginDate); diff --git a/src/Umbraco.Tests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests/TestHelpers/TestHelper.cs index a4602cecb2..5503f8021a 100644 --- a/src/Umbraco.Tests/TestHelpers/TestHelper.cs +++ b/src/Umbraco.Tests/TestHelpers/TestHelper.cs @@ -145,8 +145,8 @@ namespace Umbraco.Tests.TestHelpers Assert.Fail($"{property.DeclaringType.Name}.{property.Name}: Expected {expectedPropertyValues.Length} but got {actualPropertyValues.Length}."); for (var i = 0; i < expectedPropertyValues.Length; i++) { - Assert.AreEqual(expectedPropertyValues[i].EditValue, actualPropertyValues[i].EditValue, $"{property.DeclaringType.Name}.{property.Name}: Expected draft value \"{expectedPropertyValues[i].EditValue}\" but got \"{actualPropertyValues[i].EditValue}\"."); - Assert.AreEqual(expectedPropertyValues[i].PublishedValue, actualPropertyValues[i].PublishedValue, $"{property.DeclaringType.Name}.{property.Name}: Expected published value \"{expectedPropertyValues[i].EditValue}\" but got \"{actualPropertyValues[i].EditValue}\"."); + Assert.AreEqual(expectedPropertyValues[i].EditedValue, actualPropertyValues[i].EditedValue, $"{property.DeclaringType.Name}.{property.Name}: Expected draft value \"{expectedPropertyValues[i].EditedValue}\" but got \"{actualPropertyValues[i].EditedValue}\"."); + Assert.AreEqual(expectedPropertyValues[i].PublishedValue, actualPropertyValues[i].PublishedValue, $"{property.DeclaringType.Name}.{property.Name}: Expected published value \"{expectedPropertyValues[i].EditedValue}\" but got \"{actualPropertyValues[i].EditedValue}\"."); } } else diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Database.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Database.cs index 9582eb1f8a..916bc6207e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Database.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Database.cs @@ -1,202 +1,293 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Newtonsoft.Json; using NPoco; using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Serialization; using Umbraco.Web.Composing; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { + // fixme - use SqlTemplate for these queries else it's going to be horribly slow! + // provides efficient database access for NuCache internal class Database { + private Sql SelectContentSources(IScopeUnitOfWork uow) + { + return uow.SqlContext.Sql() + + .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), + x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), + x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) + .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) + .AndSelect(x => Alias(x.Published, "Published"), x => Alias(x.Edited, "Edited")) + + .AndSelect(x => Alias(x.VersionId, "Version")) + .AndSelect(x => Alias(x.Text, "DraftName"), x => Alias(x.VersionDate, "DraftVersionDate"), x => Alias(x.UserId, "DraftWriterId")) + .AndSelect(x => Alias(x.TemplateId, "DraftTemplateId")) + + .AndSelect("pcver", x => Alias(x.Text, "PubName"), x => Alias(x.VersionDate, "PubVersionDate"), x => Alias(x.UserId, "PubWriterId")) + .AndSelect("pdver", x => Alias(x.TemplateId, "PubTemplateId")) + + .AndSelect("nuDraft", x => Alias(x.Data, "DraftData")) + .AndSelect("nuPub", x => Alias(x.Data, "PubData")); + } + + private Sql SelectMediaSources(IScopeUnitOfWork uow) + { + return uow.SqlContext.Sql() + + .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), + x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), + x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) + .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) + //.AndSelect(x => Alias(x.Published, "Published"), x => Alias(x.Edited, "Edited")) + + .AndSelect(x => Alias(x.VersionId, "Version")) + .AndSelect(x => Alias(x.Text, "PubName"), x => Alias(x.VersionDate, "PubVersionDate"), x => Alias(x.UserId, "PubWriterId")) + .AndSelect(x => Alias(x.TemplateId, "PubTemplateId")) + + //.AndSelect("pcver", x => Alias(x.Text, "PubName"), x => Alias(x.VersionDate, "PubVersionDate"), x => Alias(x.UserId, "PubWriterId")) + //.AndSelect("pdver", x => Alias(x.TemplateId, "PubTemplateId")) + + .AndSelect("nuDraft", x => Alias(x.Data, "PubData")); + //.AndSelect("nuPub", x => Alias(x.Data, "PubData")); + } + public ContentNodeKit GetContentSource(IScopeUnitOfWork uow, int id) { - // fixme - missing things - // - // new: documentDto.publishDate - // new: documentDto.publishUserId - // new: documentDto.templateId - the published one - // fixme still, we have an issue with names - // - // node.text == contentVersion.text (and we use contentVersion.text to keep track of things) - // !! when creating a new version, the old version versionDate = publishDate, what else? - // - // new: documentDto.publishName === THE ONE WE USE FOR URLS !! - // - var dto = uow.Database.Fetch(new Sql(@"SELECT -n.id Id, n.uniqueId Uid, -uContent.contentTypeId ContentTypeId, -n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId, cver.versionId Version, -n.createDate CreateDate, n.nodeUser CreatorId, -cver.text DraftName, c.updateDate DraftVersionDate, c.writerUserId DraftWriterId, dver.templateId DraftTemplateId, doc.edited Edited, -nuDraft.data DraftData, -docPub.text PubName, doc.publishDate PubVersionDate, doc.publishUserId PubWriterId, doc.templateId PubTemplateId, doc.published Published, -nuPub.data PubData -FROM umbracoNode n -JOIN uContent c ON (c.nodeId=n.id) -LEFT JOIN uDocument doc ON (doc.nodeId=n.id) -LEFT JOIN uContentVersion cver ON (n.id=cver.nodeId AND cver.current=1) -LEFT JOIN uDocumentVersion dver ON (cver.id=dver.id) -LEFT JOIN cmsContentNu nuDraft ON (nuDraft.nodeId=n.id AND nuDraft.published=0) -LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1) -WHERE n.nodeObjectType=@objType AND n.id=@id -", new { objType = Constants.ObjectTypes.Document, /*id =*/ id })).FirstOrDefault(); + var sql = SelectContentSources(uow) + + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin().On((left, right) => left.Id == right.Id) + + .LeftJoin(j => + j.InnerJoin("pdver").On((left, right) => left.Id == right.Id && right.Published, "pcver", "pdver"), "pcver") + .On((left, right) => left.NodeId == right.NodeId) + + .LeftJoin("nuDraft").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuDraft") + .LeftJoin("nuPub").On((left, right) => left.NodeId == right.NodeId && right.Published, aliasRight: "nuPub") + + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && x.NodeId == id) + + .OrderBy(x => x.Level, x => x.SortOrder) + + ; + + var dto = uow.Database.Fetch(sql).FirstOrDefault(); return dto == null ? new ContentNodeKit() : CreateContentNodeKit(dto); } public ContentNodeKit GetMediaSource(IScopeUnitOfWork uow, int id) { - // should be only 1 version for medias + var sql = SelectMediaSources(uow) - var dto = uow.Database.Fetch(new Sql(@"SELECT -n.id Id, n.uniqueId Uid, -uContent.contentTypeId ContentTypeId, -n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId, -n.createDate CreateDate, n.nodeUser CreatorId, -n.text PubName, ver.versionId PubVersion, ver.versionDate PubVersionDate, -nuPub.data PubData -FROM umbracoNode n -JOIN uContent ON (uContent.nodeId=n.id) -JOIN uContentVersion ver ON (ver.contentId=n.id) -LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1) -WHERE n.nodeObjectType=@objType AND n.id=@id -", new { objType = Constants.ObjectTypes.Media, /*id =*/ id })).FirstOrDefault(); - return dto == null ? new ContentNodeKit() : CreateMediaNodeKit(dto); + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin().On((left, right) => left.Id == right.Id) + + //.LeftJoin(j => + // j.InnerJoin("pdver").On((left, right) => left.Id == right.Id, "pcver", "pdver"), "pcver") + //.On((left, right) => left.NodeId == right.NodeId) + + .LeftJoin("nuDraft").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuDraft") + //.LeftJoin("nuPub").On((left, right) => left.NodeId == right.NodeId && right.Published, aliasRight: "nuPub") + + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && x.NodeId == id) + + .OrderBy(x => x.Level, x => x.SortOrder) + + ; + + var dto = uow.Database.Fetch(sql).FirstOrDefault(); + return dto == null ? new ContentNodeKit() : CreateContentNodeKit(dto); } // we want arrays, we want them all loaded, not an enumerable public IEnumerable GetAllContentSources(IScopeUnitOfWork uow) { - return uow.Database.Query(new Sql(@"SELECT -n.id Id, n.uniqueId Uid, -uContent.contentTypeId ContentTypeId, -n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId, -n.createDate CreateDate, n.nodeUser CreatorId, -docDraft.text DraftName, docDraft.versionId DraftVersion, docDraft.updateDate DraftVersionDate, docDraft.writerUserId DraftWriterId, docDraft.templateId DraftTemplateId, -nuDraft.data DraftData, -docPub.text PubName, docPub.versionId PubVersion, docPub.updateDate PubVersionDate, docPub.writerUserId PubWriterId, docPub.templateId PubTemplateId, -nuPub.data PubData -FROM umbracoNode n -JOIN uContent ON (uContent.nodeId=n.id) -LEFT JOIN cmsDocument docDraft ON (docDraft.nodeId=n.id AND docDraft.newest=1 AND docDraft.published=0) -LEFT JOIN cmsDocument docPub ON (docPub.nodeId=n.id AND docPub.published=1) -LEFT JOIN cmsContentNu nuDraft ON (nuDraft.nodeId=n.id AND nuDraft.published=0) -LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1) -WHERE n.nodeObjectType=@objType -ORDER BY n.level, n.sortOrder -", new { objType = Constants.ObjectTypes.Document })).Select(CreateContentNodeKit); + var sql = SelectContentSources(uow) + + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin().On((left, right) => left.Id == right.Id) + + .LeftJoin(j => + j.InnerJoin("pdver").On((left, right) => left.Id == right.Id && right.Published, "pcver", "pdver"), "pcver") + .On((left, right) => left.NodeId == right.NodeId) + + .LeftJoin("nuDraft").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuDraft") + .LeftJoin("nuPub").On((left, right) => left.NodeId == right.NodeId && right.Published, aliasRight: "nuPub") + + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document) + + .OrderBy(x => x.Level, x => x.SortOrder) + + ; + + return uow.Database.Query(sql).Select(CreateContentNodeKit); } public IEnumerable GetAllMediaSources(IScopeUnitOfWork uow) { - // should be only 1 version for medias + var sql = SelectMediaSources(uow) - return uow.Database.Query(new Sql(@"SELECT -n.id Id, n.uniqueId Uid, -uContent.contentTypeId ContentTypeId, -n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId, -n.createDate CreateDate, n.nodeUser CreatorId, -n.text PubName, ver.versionId PubVersion, ver.versionDate PubVersionDate, -nuPub.data PubData -FROM umbracoNode n -JOIN uContent ON (uContent.nodeId=n.id) -JOIN uContentVersion ver ON (ver.contentId=n.id) -LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1) -WHERE n.nodeObjectType=@objType -ORDER BY n.level, n.sortOrder -", new { objType = Constants.ObjectTypes.Media })).Select(CreateMediaNodeKit); + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin().On((left, right) => left.Id == right.Id) + + //.LeftJoin(j => + // j.InnerJoin("pdver").On((left, right) => left.Id == right.Id, "pcver", "pdver"), "pcver") + //.On((left, right) => left.NodeId == right.NodeId) + + .LeftJoin("nuDraft").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuDraft") + //.LeftJoin("nuPub").On((left, right) => left.NodeId == right.NodeId && right.Published, aliasRight: "nuPub") + + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document) + + .OrderBy(x => x.Level, x => x.SortOrder) + + ; + + return uow.Database.Query(sql).Select(CreateMediaNodeKit); } public IEnumerable GetBranchContentSources(IScopeUnitOfWork uow, int id) { - return uow.Database.Query(new Sql(@"SELECT -n.id Id, n.uniqueId Uid, -uContent.contentTypeId ContentTypeId, -n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId, -n.createDate CreateDate, n.nodeUser CreatorId, -docDraft.text DraftName, docDraft.versionId DraftVersion, docDraft.updateDate DraftVersionDate, docDraft.writerUserId DraftWriterId, docDraft.templateId DraftTemplateId, -nuDraft.data DraftData, -docPub.text PubName, docPub.versionId PubVersion, docPub.updateDate PubVersionDate, docPub.writerUserId PubWriterId, docPub.templateId PubTemplateId, -nuPub.data PubData -FROM umbracoNode n -JOIN umbracoNode x ON (n.id=x.id OR n.path LIKE " + uow.SqlContext.SqlSyntax.GetConcat("x.path", "',%'") + @") -JOIN uContent ON (uContent.nodeId=n.id) -LEFT JOIN cmsDocument docDraft ON (docDraft.nodeId=n.id AND docDraft.newest=1 AND docDraft.published=0) -LEFT JOIN cmsDocument docPub ON (docPub.nodeId=n.id AND docPub.published=1) -LEFT JOIN cmsContentNu nuDraft ON (nuDraft.nodeId=n.id AND nuDraft.published=0) -LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1) -WHERE n.nodeObjectType=@objType AND x.id=@id -ORDER BY n.level, n.sortOrder -", new { objType = Constants.ObjectTypes.Document, /*id =*/ id })).Select(CreateContentNodeKit); + var syntax = uow.SqlContext.SqlSyntax; + var sql = SelectContentSources(uow) + + .From() + .InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x") + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin().On((left, right) => left.Id == right.Id) + + .LeftJoin(j => + j.InnerJoin("pdver").On((left, right) => left.Id == right.Id && right.Published, "pcver", "pdver"), "pcver") + .On((left, right) => left.NodeId == right.NodeId) + + .LeftJoin("nuDraft").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuDraft") + .LeftJoin("nuPub").On((left, right) => left.NodeId == right.NodeId && right.Published, aliasRight: "nuPub") + + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document) + .Where(x => x.NodeId == id, "x") + + .OrderBy(x => x.Level, x => x.SortOrder) + + ; + + return uow.Database.Query(sql).Select(CreateContentNodeKit); } public IEnumerable GetBranchMediaSources(IScopeUnitOfWork uow, int id) { - // should be only 1 version for medias + var syntax = uow.SqlContext.SqlSyntax; + var sql = SelectMediaSources(uow) - return uow.Database.Query(new Sql(@"SELECT -n.id Id, n.uniqueId Uid, -uContent.contentTypeId ContentTypeId, -n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId, -n.createDate CreateDate, n.nodeUser CreatorId, -n.text PubName, ver.versionId PubVersion, ver.versionDate PubVersionDate, -nuPub.data PubData -FROM umbracoNode n -JOIN umbracoNode x ON (n.id=x.id OR n.path LIKE " + uow.SqlContext.SqlSyntax.GetConcat("x.path", "',%'") + @") -JOIN uContent ON (uContent.nodeId=n.id) -JOIN uContentVersion ver ON (ver.contentId=n.id) -LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1) -WHERE n.nodeObjectType=@objType AND x.id=@id -ORDER BY n.level, n.sortOrder -", new { objType = Constants.ObjectTypes.Media, /*id =*/ id })).Select(CreateMediaNodeKit); + .From() + .InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x") + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin().On((left, right) => left.Id == right.Id) + + //.LeftJoin(j => + // j.InnerJoin("pdver").On((left, right) => left.Id == right.Id, "pcver", "pdver"), "pcver") + //.On((left, right) => left.NodeId == right.NodeId) + + .LeftJoin("nuDraft").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuDraft") + //.LeftJoin("nuPub").On((left, right) => left.NodeId == right.NodeId && right.Published, aliasRight: "nuPub") + + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document) + .Where(x => x.NodeId == id, "x") + + .OrderBy(x => x.Level, x => x.SortOrder) + + ; + + return uow.Database.Query(sql).Select(CreateMediaNodeKit); } public IEnumerable GetTypeContentSources(IScopeUnitOfWork uow, IEnumerable ids) { - return uow.Database.Query(new Sql(@"SELECT -n.id Id, n.uniqueId Uid, -uContent.contentTypeId ContentTypeId, -n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId, -n.createDate CreateDate, n.nodeUser CreatorId, -docDraft.text DraftName, docDraft.versionId DraftVersion, docDraft.updateDate DraftVersionDate, docDraft.writerUserId DraftWriterId, docDraft.templateId DraftTemplateId, -nuDraft.data DraftData, -docPub.text PubName, docPub.versionId PubVersion, docPub.updateDate PubVersionDate, docPub.writerUserId PubWriterId, docPub.templateId PubTemplateId, -nuPub.data PubData -FROM umbracoNode n -JOIN uContent ON (uContent.nodeId=n.id) -LEFT JOIN cmsDocument docDraft ON (docDraft.nodeId=n.id AND docDraft.newest=1 AND docDraft.published=0) -LEFT JOIN cmsDocument docPub ON (docPub.nodeId=n.id AND docPub.published=1) -LEFT JOIN cmsContentNu nuDraft ON (nuDraft.nodeId=n.id AND nuDraft.published=0) -LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1) -WHERE n.nodeObjectType=@objType AND uContent.contentTypeId IN (@ids) -ORDER BY n.level, n.sortOrder -", new { objType = Constants.ObjectTypes.Document, /*id =*/ ids })).Select(CreateContentNodeKit); + var sql = SelectContentSources(uow) + + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin().On((left, right) => left.Id == right.Id) + + .LeftJoin(j => + j.InnerJoin("pdver").On((left, right) => left.Id == right.Id && right.Published, "pcver", "pdver"), "pcver") + .On((left, right) => left.NodeId == right.NodeId) + + .LeftJoin("nuDraft").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuDraft") + .LeftJoin("nuPub").On((left, right) => left.NodeId == right.NodeId && right.Published, aliasRight: "nuPub") + + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document) + .WhereIn(x => x.ContentTypeId, ids) + + .OrderBy(x => x.Level, x => x.SortOrder) + + ; + + return uow.Database.Query(sql).Select(CreateContentNodeKit); } public IEnumerable GetTypeMediaSources(IScopeUnitOfWork uow, IEnumerable ids) { - // should be only 1 version for medias + var sql = SelectMediaSources(uow) - return uow.Database.Query(new Sql(@"SELECT -n.id Id, n.uniqueId Uid, -uContent.contentTypeId ContentTypeId, -n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId, -n.createDate CreateDate, n.nodeUser CreatorId, -n.text PubName, ver.versionId PubVersion, ver.versionDate PubVersionDate, -nuPub.data PubData -FROM umbracoNode n -JOIN uContent ON (uContent.nodeId=n.id) -JOIN uContentVersion ver ON (ver.contentId=n.id) -LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1) -WHERE n.nodeObjectType=@objType AND uContent.contentTypeId IN (@ids) -ORDER BY n.level, n.sortOrder -", new { objType = Constants.ObjectTypes.Media, /*id =*/ ids })).Select(CreateMediaNodeKit); + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin().On((left, right) => left.Id == right.Id) + + //.LeftJoin(j => + // j.InnerJoin("pdver").On((left, right) => left.Id == right.Id, "pcver", "pdver"), "pcver") + //.On((left, right) => left.NodeId == right.NodeId) + + .LeftJoin("nuDraft").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuDraft") + //.LeftJoin("nuPub").On((left, right) => left.NodeId == right.NodeId && right.Published, aliasRight: "nuPub") + + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document) + .WhereIn(x => x.ContentTypeId, ids) + + .OrderBy(x => x.Level, x => x.SortOrder) + + ; + + return uow.Database.Query(sql).Select(CreateMediaNodeKit); } private static ContentNodeKit CreateContentNodeKit(ContentSourceDto dto) @@ -208,8 +299,9 @@ ORDER BY n.level, n.sortOrder { if (dto.DraftData == null) { - //throw new Exception("Missing cmsContentNu content for node " + dto.Id + ", consider rebuilding."); - Current.Logger.Warn("Missing cmsContentNu content for node " + dto.Id + ", consider rebuilding."); + if (Debugger.IsAttached) + throw new Exception("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); + Current.Logger.Warn("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); } else { @@ -230,8 +322,9 @@ ORDER BY n.level, n.sortOrder { if (dto.PubData == null) { - //throw new Exception("Missing cmsContentNu content for node " + dto.Id + ", consider rebuilding."); - Current.Logger.Warn("Missing cmsContentNu content for node " + dto.Id + ", consider rebuilding."); + if (Debugger.IsAttached) + throw new Exception("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding."); + Current.Logger.Warn("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding."); } else { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index a22065cf32..ca2f78749b 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1074,31 +1074,18 @@ namespace Umbraco.Web.PublishedCache.NuCache private void OnContentRefreshedEntity(ContentRepository sender, ContentRepository.UnitOfWorkEntityEventArgs args) { var db = args.UnitOfWork.Database; - var content = args.Entity; + var content = (Content) args.Entity; + // always refresh the edited data OnRepositoryRefreshed(db, content, false); - // if unpublishing, remove from table - if (((Content) content).PublishedState == PublishedState.Unpublishing) - { + // if unpublishing, remove published data from table + if (content.PublishedState == PublishedState.Unpublishing) db.Execute("DELETE FROM cmsContentNu WHERE nodeId=@id AND published=1", new { id = content.Id }); - return; - } - // need to update the published data if we're saving the published version, - // or having an impact on that version - we update the published data even when masked - - IContent pc = null; - if (content.Published) - { - // saving the published version = update data - pc = content; - } - - if (pc == null) - return; - - OnRepositoryRefreshed(db, pc, true); + // if publishing, refresh the published data + else if (content.PublishedState == PublishedState.Publishing) + OnRepositoryRefreshed(db, content, true); } private void OnMediaRefreshedEntity(MediaRepository sender, MediaRepository.UnitOfWorkEntityEventArgs args) @@ -1106,12 +1093,8 @@ namespace Umbraco.Web.PublishedCache.NuCache var db = args.UnitOfWork.Database; var media = args.Entity; - // for whatever reason we delete some data when the media is trashed - // at least that's what the MediaService implementation did - if (media.Trashed) - db.Execute("DELETE FROM cmsContentXml WHERE nodeId=@id", new { id = media.Id }); - - OnRepositoryRefreshed(db, media, true); + // refresh the edited data + OnRepositoryRefreshed(db, media, false); } private void OnMemberRefreshedEntity(MemberRepository sender, MemberRepository.UnitOfWorkEntityEventArgs args) @@ -1119,6 +1102,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var db = args.UnitOfWork.Database; var member = args.Entity; + // refresh the edited data OnRepositoryRefreshed(db, member, true); } @@ -1175,7 +1159,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var data = new Dictionary(); foreach (var prop in content.Properties) { - var value = prop.GetValue(); + var value = prop.GetValue(published); //if (value != null) //{ // var e = propertyEditorResolver.GetByAlias(prop.PropertyType.PropertyEditorAlias); diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlStore.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlStore.cs index ce8dd5b193..8f1f9f01ac 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlStore.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlStore.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Text; using System.Xml; -using System.Xml.Linq; using NPoco; using Umbraco.Core; using Umbraco.Core.Configuration;