Skip to content

Commit

Permalink
Added recycle bin access via bin schema
Browse files Browse the repository at this point in the history
Fixes #471
  • Loading branch information
MarkMpn committed Jun 3, 2024
1 parent b240cbd commit 5ca3c92
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 39 deletions.
47 changes: 46 additions & 1 deletion MarkMpn.Sql4Cds.Engine/AttributeMetadataCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Metadata;
using Microsoft.Xrm.Sdk.Metadata.Query;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -21,6 +22,7 @@ public class AttributeMetadataCache : IAttributeMetadataCache
private readonly IDictionary<string, EntityMetadata> _minimalMetadata;
private readonly ISet<string> _minimalLoading;
private readonly IDictionary<string, Exception> _invalidEntities;
private readonly Lazy<string[]> _recycleBinEntities;

/// <summary>
/// Creates a new <see cref="AttributeMetadataCache"/>
Expand All @@ -34,6 +36,46 @@ public AttributeMetadataCache(IOrganizationService org)
_minimalMetadata = new Dictionary<string, EntityMetadata>(StringComparer.OrdinalIgnoreCase);
_minimalLoading = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_invalidEntities = new Dictionary<string, Exception>(StringComparer.OrdinalIgnoreCase);
_recycleBinEntities = new Lazy<string[]>(() =>
{
// Check the recyclebinconfig entity exists
try
{
_ = this["recyclebinconfig"];
}
catch
{
return null;
}

// https://learn.microsoft.com/en-us/power-apps/developer/data-platform/restore-deleted-records?tabs=sdk#detect-which-tables-are-enabled-for-recycle-bin
var qry = new FetchExpression(@"
<fetch>
<entity name='recyclebinconfig'>
<filter type='and'>
<condition attribute='statecode'
operator='eq'
value='0' />
<condition attribute='isreadyforrecyclebin'
operator='eq'
value='1' />
</filter>
<link-entity name='entity'
from='entityid'
to='extensionofrecordid'
link-type='inner'
alias='entity'>
<attribute name='logicalname' />
<order attribute='logicalname' />
</link-entity>
</entity>
</fetch>");

var resp = _org.RetrieveMultiple(qry);
return resp.Entities
.Select(e => e.GetAttributeValue<AliasedValue>("entity.logicalname").Value as string)
.ToArray();
});
}

/// <inheritdoc cref="IAttributeMetadataCache.this{string}"/>
Expand Down Expand Up @@ -118,6 +160,7 @@ public bool TryGetValue(string logicalName, out EntityMetadata metadata)
return false;
}

/// <inheritdoc/>
public bool TryGetMinimalData(string logicalName, out EntityMetadata metadata)
{
if (_metadata.TryGetValue(logicalName, out metadata))
Expand Down Expand Up @@ -202,9 +245,11 @@ public bool TryGetMinimalData(string logicalName, out EntityMetadata metadata)
}

return false;

}

/// <inheritdoc/>
public string[] RecycleBinEntities => _recycleBinEntities.Value;

public event EventHandler<MetadataLoadingEventArgs> MetadataLoading;

protected void OnMetadataLoading(MetadataLoadingEventArgs args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ Source is FetchXmlScan fetch &&
var metadata = context.DataSources[fetchXml.DataSource].Metadata;

// Aggregates are not supported on archive data
if (fetchXml.FetchXml.DataSource != null)
if (fetchXml.FetchXml.DataSource == "retained")
canUseFetchXmlAggregate = false;

// FetchXML is translated to QueryExpression for virtual entities, which doesn't support aggregates
Expand Down
11 changes: 10 additions & 1 deletion MarkMpn.Sql4Cds.Engine/ExecutionPlanBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4257,7 +4257,8 @@ private IDataExecutionPlanNodeInternal ConvertTableReference(TableReference refe

if (!String.IsNullOrEmpty(table.SchemaObject.SchemaIdentifier?.Value) &&
!table.SchemaObject.SchemaIdentifier.Value.Equals("dbo", StringComparison.OrdinalIgnoreCase) &&
!table.SchemaObject.SchemaIdentifier.Value.Equals("archive", StringComparison.OrdinalIgnoreCase))
!table.SchemaObject.SchemaIdentifier.Value.Equals("archive", StringComparison.OrdinalIgnoreCase) &&
!(table.SchemaObject.SchemaIdentifier.Value.Equals("bin", StringComparison.OrdinalIgnoreCase) && dataSource.Metadata.RecycleBinEntities != null))
throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidObjectName(table.SchemaObject));

// Validate the entity name
Expand Down Expand Up @@ -4309,6 +4310,14 @@ private IDataExecutionPlanNodeInternal ConvertTableReference(TableReference refe

fetchXmlScan.FetchXml.DataSource = "retained";
}
// Check if this should be using the recycle bin table
else if (table.SchemaObject.SchemaIdentifier?.Value.Equals("bin", StringComparison.OrdinalIgnoreCase) == true)
{
if (!dataSource.Metadata.RecycleBinEntities.Contains(meta.LogicalName))
throw new NotSupportedQueryFragmentException(Sql4CdsError.InvalidObjectName(table.SchemaObject)) { Suggestion = "Ensure restoring of deleted records is enabled for this table - see https://learn.microsoft.com/en-us/power-platform/admin/restore-deleted-table-records?WT.mc_id=DX-MVP-5004203" };

fetchXmlScan.FetchXml.DataSource = "bin";
}

return fetchXmlScan;
}
Expand Down
5 changes: 5 additions & 0 deletions MarkMpn.Sql4Cds.Engine/IAttributeMetadataCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,10 @@ public interface IAttributeMetadataCache
/// </ul>
/// </remarks>
bool TryGetMinimalData(string logicalName, out EntityMetadata metadata);

/// <summary>
/// Returns a list of entity logical names that are enabled for recycle bin access
/// </summary>
string[] RecycleBinEntities { get; }
}
}
3 changes: 3 additions & 0 deletions MarkMpn.Sql4Cds.Engine/MetaMetadataCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,5 +301,8 @@ public bool TryGetMinimalData(string logicalName, out EntityMetadata metadata)

return _inner.TryGetMinimalData(logicalName, out metadata);
}

/// <inheritdoc/>
public string[] RecycleBinEntities => _inner.RecycleBinEntities;
}
}
46 changes: 31 additions & 15 deletions MarkMpn.Sql4Cds.LanguageServer/Autocomplete/Autocomplete.cs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,12 @@ public IEnumerable<SqlAutocompleteItem> GetSuggestions(string text, int pos)
schemaName == "archive" &&
(e.IsArchivalEnabled == true || e.IsRetentionEnabled == true)
)
||
(
schemaName == "bin" &&
instance.Metadata.RecycleBinEntities != null &&
instance.Metadata.RecycleBinEntities.Contains(e.LogicalName)
)
)
);

Expand Down Expand Up @@ -770,6 +776,7 @@ private IEnumerable<SqlAutocompleteItem> AutocompleteTableName(string currentWor
}
else if (TryParseTableName(currentWord, out var instanceName, out var schemaName, out var tableName, out var parts, out var lastPartLength))
{
_dataSources.TryGetValue(instanceName, out var instance);
var lastPart = tableName;

if (parts == 1)
Expand All @@ -781,18 +788,18 @@ private IEnumerable<SqlAutocompleteItem> AutocompleteTableName(string currentWor
if (parts == 1 || (parts == 2 && _dataSources.ContainsKey(schemaName)))
{
// Could be a schema name
if ("dbo".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase))
list.Add(new SchemaAutocompleteItem("dbo", lastPartLength));
var schemaNames = (IEnumerable<string>)new[] { "dbo", "archive", "metadata" };
if (instance?.Metadata?.RecycleBinEntities != null)
schemaNames = schemaNames.Append("bin");

if ("archive".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase))
list.Add(new SchemaAutocompleteItem("archive", lastPartLength));
schemaNames = schemaNames.Where(s => s.StartsWith(lastPart, StringComparison.OrdinalIgnoreCase));

if ("metadata".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase))
list.Add(new SchemaAutocompleteItem("metadata", lastPartLength));
foreach (var schema in schemaNames)
list.Add(new SchemaAutocompleteItem(schema, lastPartLength));
}

// Could be a table name
if (_dataSources.TryGetValue(instanceName, out var instance) && instance.Entities != null)
if (instance?.Entities != null)
{
IEnumerable<EntityMetadata> entities;
IEnumerable<Message> messages = Array.Empty<Message>();
Expand All @@ -818,6 +825,11 @@ private IEnumerable<SqlAutocompleteItem> AutocompleteTableName(string currentWor
messages = instance.Messages.GetAllMessages();
}
}
else if (schemaName.Equals("bin", StringComparison.OrdinalIgnoreCase))
{
// Suggest tables that are enabled for the recycle bin
entities = instance.Entities.Where(e => instance.Metadata.RecycleBinEntities != null && instance.Metadata.RecycleBinEntities.Contains(e.LogicalName));
}
else
{
entities = Array.Empty<EntityMetadata>();
Expand Down Expand Up @@ -854,6 +866,7 @@ private IEnumerable<SqlAutocompleteItem> AutocompleteSprocName(string currentWor
}
else if (TryParseTableName(currentWord, out var instanceName, out var schemaName, out var tableName, out var parts, out var lastPartLength))
{
_dataSources.TryGetValue(instanceName, out var instance);
var lastPart = tableName;

if (parts == 1)
Expand All @@ -865,18 +878,18 @@ private IEnumerable<SqlAutocompleteItem> AutocompleteSprocName(string currentWor
if (parts == 1 || parts == 2)
{
// Could be a schema name
if ("dbo".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase))
list.Add(new SchemaAutocompleteItem("dbo", lastPartLength));
var schemaNames = (IEnumerable<string>)new[] { "dbo", "archive", "metadata" };
if (instance?.Metadata?.RecycleBinEntities != null)
schemaNames = schemaNames.Append("bin");

if ("archive".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase))
list.Add(new SchemaAutocompleteItem("archive", lastPartLength));
schemaNames = schemaNames.Where(s => s.StartsWith(lastPart, StringComparison.OrdinalIgnoreCase));

if ("metadata".StartsWith(lastPart, StringComparison.OrdinalIgnoreCase))
list.Add(new SchemaAutocompleteItem("metadata", lastPartLength));
foreach (var schema in schemaNames)
list.Add(new SchemaAutocompleteItem(schema, lastPartLength));
}

// Could be a sproc name
if (schemaName.Equals("dbo", StringComparison.OrdinalIgnoreCase) && _dataSources.TryGetValue(instanceName, out var instance) && instance.Messages != null)
if (schemaName.Equals("dbo", StringComparison.OrdinalIgnoreCase) && instance?.Messages != null)
list.AddRange(instance.Messages.GetAllMessages().Where(x => x.IsValidAsStoredProcedure()).Select(e => new SprocAutocompleteItem(e, lastPartLength)));
}

Expand Down Expand Up @@ -1498,7 +1511,10 @@ public override string ToolTipTitle

public override string ToolTipText
{
get => Text == "metadata" ? "Schema containing the metadata information" : "Schema containing the data tables";
get => Text == "metadata" ? "Schema containing the metadata information" :
Text == "archive" ? "Schema containing long-term retention tables" :
Text == "bin" ? "Schema containing recycle bin tables" :
"Schema containing the data tables";
set => base.ToolTipText = value;
}
}
Expand Down
2 changes: 2 additions & 0 deletions MarkMpn.Sql4Cds.LanguageServer/Connection/CachedMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ public bool TryGetValue(string logicalName, out EntityMetadata metadata)
return _defaultCache.TryGetValue(logicalName, out metadata);
}

public string[] RecycleBinEntities => _defaultCache.RecycleBinEntities;

public EntityMetadata[] GetAutocompleteEntities()
{
if (_cacheUnavailable && _autocompleteCache == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,17 @@ public bool HandleExpand(ExpandParams request)
});
}

if (session.DataSource.Metadata.RecycleBinEntities != null)
{
nodes.Add(new NodeInfo
{
IsLeaf = false,
Label = "Recycle Bin",
NodePath = request.NodePath + "/Bin",
NodeType = "Folder",
});
}

nodes.Add(new NodeInfo
{
IsLeaf = false,
Expand Down Expand Up @@ -205,6 +216,26 @@ public bool HandleExpand(ExpandParams request)
}
}
}
else if (url.AbsolutePath == "/Bin")
{
foreach (var entity in session.DataSource.Metadata.RecycleBinEntities)
{
nodes.Add(new NodeInfo
{
IsLeaf = false,
Label = entity,
NodePath = request.NodePath + "/" + entity,
NodeType = "Table",
Metadata = new ObjectMetadata
{
Urn = request.NodePath + "/" + entity,
MetadataType = MetadataType.Table,
Schema = "bin",
Name = entity
}
});
}
}
else if (url.AbsolutePath.StartsWith("/Tables/") && url.AbsolutePath.Split('/').Length == 3)
{
var tableName = url.AbsolutePath.Split('/')[2];
Expand Down Expand Up @@ -375,6 +406,18 @@ public bool HandleExpand(ExpandParams request)
return true;
}

private bool HasEntity(DataSourceWithInfo dataSource, string logicalName)
{
try
{
return dataSource.Metadata[logicalName] != null;
}
catch
{
return false;
}
}

private bool HandleRefresh(RefreshParams args)
{
return HandleExpand(args);
Expand Down
Loading

0 comments on commit 5ca3c92

Please sign in to comment.