2016-08-03 10:59:49 +00:00
|
|
|
//---------------------------------------------------------------------
|
|
|
|
// <copyright file="Dump.cs" company="Microsoft">
|
|
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
|
|
// </copyright>
|
|
|
|
//
|
2017-08-21 15:34:15 +00:00
|
|
|
// @owner Microsoft
|
|
|
|
// @backupOwner Microsoft
|
2016-08-03 10:59:49 +00:00
|
|
|
//---------------------------------------------------------------------
|
|
|
|
|
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Data.Common;
|
|
|
|
using System.Data.Query.PlanCompiler;
|
|
|
|
using System.Globalization;
|
|
|
|
using System.Text;
|
|
|
|
using System.Diagnostics;
|
|
|
|
using System.IO;
|
|
|
|
using System.Xml;
|
|
|
|
using md = System.Data.Metadata.Edm;
|
|
|
|
|
|
|
|
//
|
|
|
|
// This module serves as a dump routine for an IQT
|
|
|
|
// The output is a weird form of Sql - closer to Quel (and perhaps, C#
|
|
|
|
// comprehensions)
|
|
|
|
//
|
|
|
|
namespace System.Data.Query.InternalTrees {
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// A dump module for the Iqt
|
|
|
|
/// </summary>
|
|
|
|
internal class Dump : BasicOpVisitor, IDisposable {
|
|
|
|
|
|
|
|
#region private state
|
|
|
|
|
|
|
|
private XmlWriter _writer;
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region constructors
|
|
|
|
|
|
|
|
private Dump(Stream stream)
|
|
|
|
: this(stream, Dump.DefaultEncoding, true) { }
|
|
|
|
|
|
|
|
private Dump(Stream stream, Encoding encoding, bool indent)
|
|
|
|
: base() {
|
|
|
|
XmlWriterSettings settings = new XmlWriterSettings();
|
|
|
|
settings.CheckCharacters = false;
|
|
|
|
settings.Indent = true;
|
|
|
|
settings.Encoding = encoding;
|
|
|
|
_writer = XmlWriter.Create(stream, settings);
|
|
|
|
_writer.WriteStartDocument(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region "public" surface
|
|
|
|
|
|
|
|
internal static readonly Encoding DefaultEncoding = Encoding.UTF8;
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Driver method to dump the entire tree
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="itree"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
static internal string ToXml(Command itree) {
|
|
|
|
return ToXml(itree, itree.Root);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Driver method to dump the a subtree of a tree
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="itree"></param>
|
|
|
|
/// <param name="subtree"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
static internal string ToXml(Command itree, Node subtree) {
|
|
|
|
MemoryStream stream = new MemoryStream();
|
|
|
|
|
|
|
|
using (Dump dumper = new Dump(stream)) {
|
|
|
|
// Just in case the node we're provided doesn't dump as XML, we'll always stick
|
|
|
|
// an XML wrapper around it -- this happens when we're dumping scalarOps, for
|
|
|
|
// example, and it's unfortunate if you can't debug them using a dump...
|
|
|
|
using (new AutoXml(dumper, "nodes")) {
|
|
|
|
dumper.VisitNode(subtree);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return DefaultEncoding.GetString(stream.ToArray());
|
|
|
|
}
|
|
|
|
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "System.Data.Query.InternalTrees.Dump.ToXml")]
|
|
|
|
static internal string ToXml(ColumnMap columnMap) {
|
|
|
|
MemoryStream stream = new MemoryStream();
|
|
|
|
|
|
|
|
using (Dump dumper = new Dump(stream)) {
|
|
|
|
// Just in case the node we're provided doesn't dump as XML, we'll always stick
|
|
|
|
// an XML wrapper around it -- this happens when we're dumping scalarOps, for
|
|
|
|
// example, and it's unfortunate if you can't debug them using a dump...
|
|
|
|
using (new AutoXml(dumper, "columnMap")) {
|
|
|
|
columnMap.Accept(ColumnMapDumper.Instance, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return DefaultEncoding.GetString(stream.ToArray());
|
|
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Begin/End management
|
|
|
|
|
|
|
|
void IDisposable.Dispose() {
|
|
|
|
// Technically, calling GC.SuppressFinalize is not required because the class does not
|
|
|
|
// have a finalizer, but it does no harm, protects against the case where a finalizer is added
|
|
|
|
// in the future, and prevents an FxCop warning.
|
|
|
|
GC.SuppressFinalize(this);
|
|
|
|
try {
|
|
|
|
_writer.WriteEndDocument();
|
|
|
|
_writer.Flush();
|
|
|
|
_writer.Close();
|
|
|
|
}
|
|
|
|
catch (Exception e) {
|
|
|
|
if (!EntityUtil.IsCatchableExceptionType(e)) {
|
|
|
|
throw;
|
|
|
|
}
|
|
|
|
// eat this exception; we don't care if the dumper is failing...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
internal void Begin(string name, Dictionary<string, object> attrs) {
|
|
|
|
_writer.WriteStartElement(name);
|
|
|
|
if (attrs != null) {
|
|
|
|
foreach (KeyValuePair<string, object> attr in attrs) {
|
|
|
|
_writer.WriteAttributeString(attr.Key, attr.Value.ToString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
internal void BeginExpression() {
|
|
|
|
WriteString("(");
|
|
|
|
}
|
|
|
|
|
|
|
|
internal void EndExpression() {
|
|
|
|
WriteString(")");
|
|
|
|
}
|
|
|
|
|
|
|
|
internal void End(string name) {
|
|
|
|
_writer.WriteEndElement();
|
|
|
|
}
|
|
|
|
|
|
|
|
internal void WriteString(string value) {
|
|
|
|
_writer.WriteString(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region VisitorMethods
|
|
|
|
|
|
|
|
protected override void VisitDefault(Node n) {
|
|
|
|
using (new AutoXml(this, n.Op)) {
|
|
|
|
base.VisitDefault(n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override void VisitScalarOpDefault(ScalarOp op, Node n) {
|
|
|
|
using (new AutoString(this, op)) {
|
|
|
|
string separator = string.Empty;
|
|
|
|
foreach (Node chi in n.Children) {
|
|
|
|
WriteString(separator);
|
|
|
|
VisitNode(chi);
|
|
|
|
separator = ",";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override void VisitJoinOp(JoinBaseOp op, Node n) {
|
|
|
|
using (new AutoXml(this, op)) {
|
|
|
|
if (n.Children.Count > 2) {
|
|
|
|
using (new AutoXml(this, "condition")) {
|
|
|
|
VisitNode(n.Child2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
using (new AutoXml(this, "input")) {
|
|
|
|
VisitNode(n.Child0);
|
|
|
|
}
|
|
|
|
using (new AutoXml(this, "input")) {
|
|
|
|
VisitNode(n.Child1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(CaseOp op, Node n) {
|
|
|
|
using (new AutoXml(this, op)) {
|
|
|
|
int i = 0;
|
|
|
|
while (i < n.Children.Count) {
|
|
|
|
if ((i + 1) < n.Children.Count) {
|
|
|
|
using (new AutoXml(this, "when")) {
|
|
|
|
VisitNode(n.Children[i++]);
|
|
|
|
}
|
|
|
|
using (new AutoXml(this, "then")) {
|
|
|
|
VisitNode(n.Children[i++]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
using (new AutoXml(this, "else")) {
|
|
|
|
VisitNode(n.Children[i++]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(CollectOp op, Node n) {
|
|
|
|
using (new AutoXml(this, op)) {
|
|
|
|
VisitChildren(n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override void VisitConstantOp(ConstantBaseOp op, Node n) {
|
|
|
|
using (new AutoString(this, op)) {
|
|
|
|
if (null == op.Value) {
|
|
|
|
WriteString("null");
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
WriteString("(");
|
|
|
|
WriteString(op.Type.EdmType.FullName);
|
|
|
|
WriteString(")");
|
|
|
|
WriteString(String.Format(CultureInfo.InvariantCulture,"{0}",op.Value));
|
|
|
|
}
|
|
|
|
VisitChildren(n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(DistinctOp op, Node n) {
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string, object>();
|
|
|
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
string separator = string.Empty;
|
|
|
|
|
|
|
|
foreach (Var v in op.Keys) {
|
|
|
|
sb.Append(separator);
|
|
|
|
sb.Append(v.Id);
|
|
|
|
separator = ",";
|
|
|
|
}
|
|
|
|
if (0 != sb.Length) {
|
|
|
|
attrs.Add("Keys", sb.ToString());
|
|
|
|
}
|
|
|
|
|
|
|
|
using (new AutoXml(this, op, attrs)) {
|
|
|
|
VisitChildren(n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override void VisitGroupByOp(GroupByBaseOp op, Node n) {
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string, object>();
|
|
|
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
string separator = string.Empty;
|
|
|
|
|
|
|
|
foreach (Var v in op.Keys) {
|
|
|
|
sb.Append(separator);
|
|
|
|
sb.Append(v.Id);
|
|
|
|
separator = ",";
|
|
|
|
}
|
|
|
|
if (0 != sb.Length) {
|
|
|
|
attrs.Add("Keys", sb.ToString());
|
|
|
|
}
|
|
|
|
|
|
|
|
using (new AutoXml(this, op, attrs)) {
|
|
|
|
using (new AutoXml(this, "outputs")) {
|
|
|
|
foreach (Var v in op.Outputs) {
|
|
|
|
DumpVar(v);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
VisitChildren(n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(IsOfOp op, Node n)
|
|
|
|
{
|
|
|
|
using (new AutoXml(this, ( op.IsOfOnly ? "IsOfOnly" : "IsOf" )))
|
|
|
|
{
|
|
|
|
string separator = string.Empty;
|
|
|
|
foreach (Node chi in n.Children)
|
|
|
|
{
|
|
|
|
WriteString(separator);
|
|
|
|
VisitNode(chi);
|
|
|
|
separator = ",";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override void VisitNestOp(NestBaseOp op, Node n) {
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string, object>();
|
|
|
|
|
|
|
|
SingleStreamNestOp ssnOp = op as SingleStreamNestOp;
|
|
|
|
if (null != ssnOp) {
|
|
|
|
attrs.Add("Discriminator", (ssnOp.Discriminator == null) ? "<null>" : ssnOp.Discriminator.ToString());
|
|
|
|
}
|
|
|
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
string separator;
|
|
|
|
|
|
|
|
if (null != ssnOp) {
|
|
|
|
sb.Length = 0;
|
|
|
|
separator = string.Empty;
|
|
|
|
foreach (Var v in ssnOp.Keys) {
|
|
|
|
sb.Append(separator);
|
|
|
|
sb.Append(v.Id);
|
|
|
|
separator = ",";
|
|
|
|
}
|
|
|
|
if (0 != sb.Length) {
|
|
|
|
attrs.Add("Keys", sb.ToString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
using (new AutoXml(this, op, attrs)) {
|
|
|
|
using (new AutoXml(this, "outputs")) {
|
|
|
|
foreach (Var v in op.Outputs) {
|
|
|
|
DumpVar(v);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
foreach (CollectionInfo ci in op.CollectionInfo) {
|
|
|
|
Dictionary<string, object> attrs2 = new Dictionary<string, object>();
|
|
|
|
attrs2.Add("CollectionVar", ci.CollectionVar);
|
|
|
|
|
|
|
|
if (null != ci.DiscriminatorValue) {
|
|
|
|
attrs2.Add("DiscriminatorValue", ci.DiscriminatorValue);
|
|
|
|
}
|
|
|
|
if (0 != ci.FlattenedElementVars.Count) {
|
|
|
|
attrs2.Add("FlattenedElementVars", FormatVarList(sb, ci.FlattenedElementVars));
|
|
|
|
}
|
|
|
|
if (0 != ci.Keys.Count) {
|
|
|
|
attrs2.Add("Keys", ci.Keys);
|
|
|
|
}
|
|
|
|
if (0 != ci.SortKeys.Count) {
|
|
|
|
attrs2.Add("SortKeys", FormatVarList(sb, ci.SortKeys));
|
|
|
|
}
|
|
|
|
using (new AutoXml(this, "collection", attrs2)) {
|
|
|
|
ci.ColumnMap.Accept(ColumnMapDumper.Instance, this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
VisitChildren(n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static string FormatVarList(StringBuilder sb, VarList varList) {
|
|
|
|
string separator;
|
|
|
|
sb.Length = 0;
|
|
|
|
separator = string.Empty;
|
|
|
|
foreach (Var v in varList) {
|
|
|
|
sb.Append(separator);
|
|
|
|
sb.Append(v.Id);
|
|
|
|
separator = ",";
|
|
|
|
}
|
|
|
|
return sb.ToString();
|
|
|
|
}
|
|
|
|
|
|
|
|
private static string FormatVarList(StringBuilder sb, List<SortKey> varList) {
|
|
|
|
string separator;
|
|
|
|
sb.Length = 0;
|
|
|
|
separator = string.Empty;
|
|
|
|
foreach (SortKey v in varList) {
|
|
|
|
sb.Append(separator);
|
|
|
|
sb.Append(v.Var.Id);
|
|
|
|
separator = ",";
|
|
|
|
}
|
|
|
|
return sb.ToString();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void VisitNewOp(Op op, Node n) {
|
|
|
|
using (new AutoXml(this, op)) {
|
|
|
|
foreach (Node chi in n.Children) {
|
|
|
|
using (new AutoXml(this, "argument", null)) {
|
|
|
|
VisitNode(chi);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
public override void Visit(NewEntityOp op, Node n) {
|
|
|
|
VisitNewOp(op, n);
|
|
|
|
}
|
|
|
|
public override void Visit(NewInstanceOp op, Node n) {
|
|
|
|
VisitNewOp(op, n);
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(DiscriminatedNewEntityOp op, Node n) {
|
|
|
|
VisitNewOp(op, n);
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(NewMultisetOp op, Node n) {
|
|
|
|
VisitNewOp(op, n);
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(NewRecordOp op, Node n) {
|
|
|
|
VisitNewOp(op, n);
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(PhysicalProjectOp op, Node n) {
|
|
|
|
using (new AutoXml(this, op)) {
|
|
|
|
using (new AutoXml(this, "outputs")) {
|
|
|
|
foreach (Var v in op.Outputs) {
|
|
|
|
DumpVar(v);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
using (new AutoXml(this, "columnMap")) {
|
|
|
|
op.ColumnMap.Accept(ColumnMapDumper.Instance, this);
|
|
|
|
}
|
|
|
|
using (new AutoXml(this, "input")) {
|
|
|
|
VisitChildren(n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(ProjectOp op, Node n) {
|
|
|
|
|
|
|
|
using (new AutoXml(this, op)) {
|
|
|
|
using (new AutoXml(this, "outputs")) {
|
|
|
|
foreach (Var v in op.Outputs) {
|
|
|
|
DumpVar(v);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
VisitChildren(n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(PropertyOp op, Node n) {
|
|
|
|
using (new AutoString(this, op)) {
|
|
|
|
VisitChildren(n);
|
|
|
|
WriteString(".");
|
|
|
|
WriteString(op.PropertyInfo.Name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
public override void Visit(RelPropertyOp op, Node n) {
|
|
|
|
using (new AutoString(this, op)) {
|
|
|
|
VisitChildren(n);
|
|
|
|
WriteString(".NAVIGATE(");
|
|
|
|
WriteString(op.PropertyInfo.Relationship.Name);
|
|
|
|
WriteString(",");
|
|
|
|
WriteString(op.PropertyInfo.FromEnd.Name);
|
|
|
|
WriteString(",");
|
|
|
|
WriteString(op.PropertyInfo.ToEnd.Name);
|
|
|
|
WriteString(")");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(ScanTableOp op, Node n) {
|
|
|
|
using (new AutoXml(this, op)) {
|
|
|
|
DumpTable(op.Table);
|
|
|
|
VisitChildren(n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public override void Visit(ScanViewOp op, Node n) {
|
|
|
|
using (new AutoXml(this, op)) {
|
|
|
|
DumpTable(op.Table);
|
|
|
|
VisitChildren(n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override void VisitSetOp(SetOp op, Node n) {
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string, object>();
|
|
|
|
if (OpType.UnionAll == op.OpType) {
|
|
|
|
UnionAllOp uallOp = (UnionAllOp)op;
|
|
|
|
if (null != uallOp.BranchDiscriminator) {
|
|
|
|
attrs.Add("branchDiscriminator", uallOp.BranchDiscriminator);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
using (new AutoXml(this, op, attrs)) {
|
|
|
|
using (new AutoXml(this, "outputs")) {
|
|
|
|
foreach (Var v in op.Outputs) {
|
|
|
|
DumpVar(v);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
int i = 0;
|
|
|
|
foreach (Node chi in n.Children) {
|
|
|
|
Dictionary<string, object> attrs2 = new Dictionary<string, object>();
|
|
|
|
attrs2.Add("VarMap", op.VarMap[i++].ToString());
|
|
|
|
|
|
|
|
using (new AutoXml(this, "input", attrs2)) {
|
|
|
|
VisitNode(chi);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(SortOp op, Node n) {
|
|
|
|
using (new AutoXml(this, op)) {
|
|
|
|
base.Visit(op, n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(ConstrainedSortOp op, Node n) {
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string, object>();
|
|
|
|
attrs.Add("WithTies", op.WithTies);
|
|
|
|
using (new AutoXml(this, op, attrs)) {
|
|
|
|
base.Visit(op, n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override void VisitSortOp(SortBaseOp op, Node n)
|
|
|
|
{
|
|
|
|
using (new AutoXml(this, "keys")) {
|
|
|
|
foreach (InternalTrees.SortKey sortKey in op.Keys) {
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string, object>();
|
|
|
|
attrs.Add("Var", sortKey.Var);
|
|
|
|
attrs.Add("Ascending", sortKey.AscendingSort);
|
|
|
|
attrs.Add("Collation", sortKey.Collation);
|
|
|
|
|
|
|
|
using (new AutoXml(this, "sortKey", attrs)) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
VisitChildren(n);
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(UnnestOp op, Node n) {
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string, object>();
|
|
|
|
if (null != op.Var) {
|
|
|
|
attrs.Add("Var", op.Var.Id);
|
|
|
|
}
|
|
|
|
using (new AutoXml(this, op, attrs)) {
|
|
|
|
DumpTable(op.Table);
|
|
|
|
VisitChildren(n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(VarDefOp op, Node n) {
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string, object>();
|
|
|
|
attrs.Add("Var", op.Var.Id);
|
|
|
|
|
|
|
|
using (new AutoXml(this, op, attrs)) {
|
|
|
|
VisitChildren(n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Visit(VarRefOp op, Node n) {
|
|
|
|
using (new AutoString(this, op)) {
|
|
|
|
VisitChildren(n);
|
|
|
|
if (null != op.Type) {
|
|
|
|
WriteString("Type=");
|
|
|
|
WriteString(TypeHelpers.GetFullName(op.Type));
|
|
|
|
WriteString(", ");
|
|
|
|
}
|
|
|
|
WriteString("Var=");
|
|
|
|
WriteString(op.Var.Id.ToString(CultureInfo.InvariantCulture));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region dumper helpers
|
|
|
|
|
|
|
|
private void DumpVar(Var v) {
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string, object>();
|
|
|
|
|
|
|
|
attrs.Add("Var", v.Id);
|
|
|
|
ColumnVar cv = v as ColumnVar;
|
|
|
|
if (null != cv) {
|
|
|
|
attrs.Add("Name", cv.ColumnMetadata.Name);
|
|
|
|
attrs.Add("Type", TypeHelpers.GetFullName(cv.ColumnMetadata.Type));
|
|
|
|
}
|
|
|
|
using (new AutoXml(this, v.GetType().Name, attrs)) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void DumpVars(List<Var> vars) {
|
|
|
|
foreach (Var v in vars) {
|
|
|
|
DumpVar(v);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void DumpTable(Table table) {
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string, object>();
|
|
|
|
|
|
|
|
attrs.Add("Table", table.TableId);
|
|
|
|
if (null != table.TableMetadata.Extent) {
|
|
|
|
attrs.Add("Extent", table.TableMetadata.Extent.Name);
|
|
|
|
}
|
|
|
|
|
|
|
|
using (new AutoXml(this, "Table", attrs)) {
|
|
|
|
DumpVars(table.Columns);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#region ColumnMap dumper
|
|
|
|
internal class ColumnMapDumper : ColumnMapVisitor<Dump> {
|
|
|
|
|
|
|
|
static internal ColumnMapDumper Instance = new ColumnMapDumper();
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Private constructor
|
|
|
|
/// </summary>
|
|
|
|
private ColumnMapDumper()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
#region Helpers
|
|
|
|
/// <summary>
|
|
|
|
/// Common CollectionColumnMap code
|
|
|
|
/// </summary>
|
|
|
|
private void DumpCollection(CollectionColumnMap columnMap, Dump dumper) {
|
|
|
|
if (columnMap.ForeignKeys.Length > 0) {
|
|
|
|
using (new AutoXml(dumper, "foreignKeys")) {
|
|
|
|
VisitList(columnMap.ForeignKeys, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (columnMap.Keys.Length > 0) {
|
|
|
|
using (new AutoXml(dumper, "keys")) {
|
|
|
|
VisitList(columnMap.Keys, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
using (new AutoXml(dumper, "element")) {
|
|
|
|
columnMap.Element.Accept(this, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Common code to produce an the attributes for the dumper's XML node
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="columnMap"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
private static Dictionary<string, object> GetAttributes(ColumnMap columnMap) {
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string, object>();
|
|
|
|
attrs.Add("Type", columnMap.Type.ToString());
|
|
|
|
return attrs;
|
|
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// ComplexTypeColumnMap
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="columnMap"></param>
|
|
|
|
/// <param name="dumper"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
internal override void Visit(ComplexTypeColumnMap columnMap, Dump dumper) {
|
|
|
|
using (new AutoXml(dumper, "ComplexType", GetAttributes(columnMap))) {
|
|
|
|
if (columnMap.NullSentinel != null) {
|
|
|
|
using (new AutoXml(dumper, "nullSentinel")) {
|
|
|
|
columnMap.NullSentinel.Accept(this, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
VisitList(columnMap.Properties, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// DiscriminatedCollectionColumnMap
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="columnMap"></param>
|
|
|
|
/// <param name="dumper"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
internal override void Visit(DiscriminatedCollectionColumnMap columnMap, Dump dumper) {
|
|
|
|
using (new AutoXml(dumper, "DiscriminatedCollection", GetAttributes(columnMap))) {
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string,object>();
|
|
|
|
attrs.Add("Value", columnMap.DiscriminatorValue);
|
|
|
|
|
|
|
|
using (new AutoXml(dumper, "discriminator", attrs)) {
|
|
|
|
columnMap.Discriminator.Accept(this, dumper);
|
|
|
|
}
|
|
|
|
DumpCollection(columnMap, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// EntityColumnMap
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="columnMap"></param>
|
|
|
|
/// <param name="dumper"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
internal override void Visit(EntityColumnMap columnMap, Dump dumper) {
|
|
|
|
using (new AutoXml(dumper, "Entity", GetAttributes(columnMap))) {
|
|
|
|
using (new AutoXml(dumper, "entityIdentity")) {
|
|
|
|
VisitEntityIdentity(columnMap.EntityIdentity, dumper);
|
|
|
|
}
|
|
|
|
VisitList(columnMap.Properties, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// PolymorphicColumnMap
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="columnMap"></param>
|
|
|
|
/// <param name="dumper"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
internal override void Visit(SimplePolymorphicColumnMap columnMap, Dump dumper) {
|
|
|
|
using (new AutoXml(dumper, "SimplePolymorphic", GetAttributes(columnMap))) {
|
|
|
|
using (new AutoXml(dumper, "typeDiscriminator")) {
|
|
|
|
columnMap.TypeDiscriminator.Accept(this, dumper);
|
|
|
|
}
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string, object>();
|
|
|
|
foreach (KeyValuePair<object, TypedColumnMap> tc in columnMap.TypeChoices) {
|
|
|
|
attrs.Clear();
|
|
|
|
attrs.Add("DiscriminatorValue", tc.Key);
|
|
|
|
using (new AutoXml(dumper, "choice", attrs)) {
|
|
|
|
tc.Value.Accept(this, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
using (new AutoXml(dumper, "default")) {
|
|
|
|
VisitList(columnMap.Properties, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// MultipleDiscriminatorPolymorphicColumnMap
|
|
|
|
/// </summary>
|
|
|
|
internal override void Visit(MultipleDiscriminatorPolymorphicColumnMap columnMap, Dump dumper) {
|
|
|
|
using (new AutoXml(dumper, "MultipleDiscriminatorPolymorphic", GetAttributes(columnMap))) {
|
|
|
|
using (new AutoXml(dumper, "typeDiscriminators")) {
|
|
|
|
VisitList(columnMap.TypeDiscriminators, dumper);
|
|
|
|
}
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string, object>();
|
|
|
|
foreach (var tc in columnMap.TypeChoices) {
|
|
|
|
attrs.Clear();
|
|
|
|
attrs.Add("EntityType", tc.Key);
|
|
|
|
using (new AutoXml(dumper, "choice", attrs)) {
|
|
|
|
tc.Value.Accept(this, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
using (new AutoXml(dumper, "default")) {
|
|
|
|
VisitList(columnMap.Properties, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// RecordColumnMap
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="columnMap"></param>
|
|
|
|
/// <param name="dumper"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
internal override void Visit(RecordColumnMap columnMap, Dump dumper) {
|
|
|
|
using (new AutoXml(dumper, "Record", GetAttributes(columnMap))) {
|
|
|
|
if (columnMap.NullSentinel != null) {
|
|
|
|
using (new AutoXml(dumper, "nullSentinel")) {
|
|
|
|
columnMap.NullSentinel.Accept(this, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
VisitList(columnMap.Properties, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// RefColumnMap
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="columnMap"></param>
|
|
|
|
/// <param name="dumper"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
internal override void Visit(RefColumnMap columnMap, Dump dumper) {
|
|
|
|
using (new AutoXml(dumper, "Ref", GetAttributes(columnMap))) {
|
|
|
|
using (new AutoXml(dumper, "entityIdentity")) {
|
|
|
|
VisitEntityIdentity(columnMap.EntityIdentity, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// SimpleCollectionColumnMap
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="columnMap"></param>
|
|
|
|
/// <param name="dumper"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
internal override void Visit(SimpleCollectionColumnMap columnMap, Dump dumper) {
|
|
|
|
using (new AutoXml(dumper, "SimpleCollection", GetAttributes(columnMap))) {
|
|
|
|
DumpCollection(columnMap, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// SimpleColumnMap
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="columnMap"></param>
|
|
|
|
/// <param name="dumper"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
internal override void Visit(ScalarColumnMap columnMap, Dump dumper) {
|
|
|
|
Dictionary<string, object> attrs = GetAttributes(columnMap);
|
|
|
|
attrs.Add("CommandId", columnMap.CommandId);
|
|
|
|
attrs.Add("ColumnPos", columnMap.ColumnPos);
|
|
|
|
|
|
|
|
using (new AutoXml(dumper, "AssignedSimple", attrs)) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// SimpleColumnMap
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="columnMap"></param>
|
|
|
|
/// <param name="dumper"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
internal override void Visit(VarRefColumnMap columnMap, Dump dumper) {
|
|
|
|
Dictionary<string, object> attrs = GetAttributes(columnMap);
|
|
|
|
attrs.Add("Var", ((VarRefColumnMap)columnMap).Var.Id);
|
|
|
|
using (new AutoXml(dumper, "VarRef", attrs)) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// DiscriminatedEntityIdentity
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="entityIdentity"></param>
|
|
|
|
/// <param name="dumper"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
protected override void VisitEntityIdentity(DiscriminatedEntityIdentity entityIdentity, Dump dumper) {
|
|
|
|
using (new AutoXml(dumper, "DiscriminatedEntityIdentity")) {
|
|
|
|
using (new AutoXml(dumper, "entitySetId")) {
|
|
|
|
entityIdentity.EntitySetColumnMap.Accept(this, dumper);
|
|
|
|
}
|
|
|
|
if (entityIdentity.Keys.Length > 0) {
|
|
|
|
using (new AutoXml(dumper, "keys")) {
|
|
|
|
VisitList(entityIdentity.Keys, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// SimpleEntityIdentity
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="entityIdentity"></param>
|
|
|
|
/// <param name="dumper"></param>
|
|
|
|
/// <returns></returns>
|
|
|
|
protected override void VisitEntityIdentity(SimpleEntityIdentity entityIdentity, Dump dumper) {
|
|
|
|
using (new AutoXml(dumper, "SimpleEntityIdentity")) {
|
|
|
|
if (entityIdentity.Keys.Length > 0) {
|
|
|
|
using (new AutoXml(dumper, "keys")) {
|
|
|
|
VisitList(entityIdentity.Keys, dumper);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
internal struct AutoString : IDisposable {
|
|
|
|
private Dump _dumper;
|
|
|
|
|
|
|
|
internal AutoString(Dump dumper, Op op) {
|
|
|
|
_dumper = dumper;
|
|
|
|
_dumper.WriteString(AutoString.ToString(op.OpType));
|
|
|
|
_dumper.BeginExpression();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void Dispose() {
|
|
|
|
try {
|
|
|
|
_dumper.EndExpression();
|
|
|
|
}
|
|
|
|
catch (Exception e) {
|
|
|
|
if (!EntityUtil.IsCatchableExceptionType(e)) {
|
|
|
|
throw;
|
|
|
|
}
|
|
|
|
// eat this exception; we don't care if the dumper is failing...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
internal static string ToString(OpType op)
|
|
|
|
{ // perf: Enum.ToString() actually is very perf intensive in time & memory
|
|
|
|
switch (op)
|
|
|
|
{
|
|
|
|
case OpType.Aggregate:
|
|
|
|
return "Aggregate";
|
|
|
|
case OpType.And:
|
|
|
|
return "And";
|
|
|
|
case OpType.Case:
|
|
|
|
return "Case";
|
|
|
|
case OpType.Cast:
|
|
|
|
return "Cast";
|
|
|
|
case OpType.Collect:
|
|
|
|
return "Collect";
|
|
|
|
case OpType.Constant:
|
|
|
|
return "Constant";
|
|
|
|
case OpType.ConstantPredicate:
|
|
|
|
return "ConstantPredicate";
|
|
|
|
case OpType.CrossApply:
|
|
|
|
return "CrossApply";
|
|
|
|
case OpType.CrossJoin:
|
|
|
|
return "CrossJoin";
|
|
|
|
case OpType.Deref:
|
|
|
|
return "Deref";
|
|
|
|
case OpType.Distinct:
|
|
|
|
return "Distinct";
|
|
|
|
case OpType.Divide:
|
|
|
|
return "Divide";
|
|
|
|
case OpType.Element:
|
|
|
|
return "Element";
|
|
|
|
case OpType.EQ:
|
|
|
|
return "EQ";
|
|
|
|
case OpType.Except:
|
|
|
|
return "Except";
|
|
|
|
case OpType.Exists:
|
|
|
|
return "Exists";
|
|
|
|
case OpType.Filter:
|
|
|
|
return "Filter";
|
|
|
|
case OpType.FullOuterJoin:
|
|
|
|
return "FullOuterJoin";
|
|
|
|
case OpType.Function:
|
|
|
|
return "Function";
|
|
|
|
case OpType.GE:
|
|
|
|
return "GE";
|
|
|
|
case OpType.GetEntityRef:
|
|
|
|
return "GetEntityRef";
|
|
|
|
case OpType.GetRefKey:
|
|
|
|
return "GetRefKey";
|
|
|
|
case OpType.GroupBy:
|
|
|
|
return "GroupBy";
|
|
|
|
case OpType.GroupByInto:
|
|
|
|
return "GroupByInto";
|
|
|
|
case OpType.GT:
|
|
|
|
return "GT";
|
|
|
|
case OpType.InnerJoin:
|
|
|
|
return "InnerJoin";
|
|
|
|
case OpType.InternalConstant:
|
|
|
|
return "InternalConstant";
|
|
|
|
case OpType.Intersect:
|
|
|
|
return "Intersect";
|
|
|
|
case OpType.IsNull:
|
|
|
|
return "IsNull";
|
|
|
|
case OpType.IsOf:
|
|
|
|
return "IsOf";
|
|
|
|
case OpType.LE:
|
|
|
|
return "LE";
|
|
|
|
case OpType.Leaf:
|
|
|
|
return "Leaf";
|
|
|
|
case OpType.LeftOuterJoin:
|
|
|
|
return "LeftOuterJoin";
|
|
|
|
case OpType.Like:
|
|
|
|
return "Like";
|
|
|
|
case OpType.LT:
|
|
|
|
return "LT";
|
|
|
|
case OpType.Minus:
|
|
|
|
return "Minus";
|
|
|
|
case OpType.Modulo:
|
|
|
|
return "Modulo";
|
|
|
|
case OpType.Multiply:
|
|
|
|
return "Multiply";
|
|
|
|
case OpType.MultiStreamNest:
|
|
|
|
return "MultiStreamNest";
|
|
|
|
case OpType.Navigate:
|
|
|
|
return "Navigate";
|
|
|
|
case OpType.NE:
|
|
|
|
return "NE";
|
|
|
|
case OpType.NewEntity:
|
|
|
|
return "NewEntity";
|
|
|
|
case OpType.NewInstance:
|
|
|
|
return "NewInstance";
|
|
|
|
case OpType.DiscriminatedNewEntity:
|
|
|
|
return "DiscriminatedNewEntity";
|
|
|
|
case OpType.NewMultiset:
|
|
|
|
return "NewMultiset";
|
|
|
|
case OpType.NewRecord:
|
|
|
|
return "NewRecord";
|
|
|
|
case OpType.Not:
|
|
|
|
return "Not";
|
|
|
|
case OpType.Null:
|
|
|
|
return "Null";
|
|
|
|
case OpType.NullSentinel:
|
|
|
|
return "NullSentinel";
|
|
|
|
case OpType.Or:
|
|
|
|
return "Or";
|
|
|
|
case OpType.OuterApply:
|
|
|
|
return "OuterApply";
|
|
|
|
case OpType.PhysicalProject:
|
|
|
|
return "PhysicalProject";
|
|
|
|
case OpType.Plus:
|
|
|
|
return "Plus";
|
|
|
|
case OpType.Project:
|
|
|
|
return "Project";
|
|
|
|
case OpType.Property:
|
|
|
|
return "Property";
|
|
|
|
case OpType.Ref:
|
|
|
|
return "Ref";
|
|
|
|
case OpType.RelProperty:
|
|
|
|
return "RelProperty";
|
|
|
|
case OpType.ScanTable:
|
|
|
|
return "ScanTable";
|
|
|
|
case OpType.ScanView:
|
|
|
|
return "ScanView";
|
|
|
|
case OpType.SingleRow:
|
|
|
|
return "SingleRow";
|
|
|
|
case OpType.SingleRowTable:
|
|
|
|
return "SingleRowTable";
|
|
|
|
case OpType.SingleStreamNest:
|
|
|
|
return "SingleStreamNest";
|
|
|
|
case OpType.SoftCast:
|
|
|
|
return "SoftCast";
|
|
|
|
case OpType.Sort:
|
|
|
|
return "Sort";
|
|
|
|
case OpType.Treat:
|
|
|
|
return "Treat";
|
|
|
|
case OpType.UnaryMinus:
|
|
|
|
return "UnaryMinus";
|
|
|
|
case OpType.UnionAll:
|
|
|
|
return "UnionAll";
|
|
|
|
case OpType.Unnest:
|
|
|
|
return "Unnest";
|
|
|
|
case OpType.VarDef:
|
|
|
|
return "VarDef";
|
|
|
|
case OpType.VarDefList:
|
|
|
|
return "VarDefList";
|
|
|
|
case OpType.VarRef:
|
|
|
|
return "VarRef";
|
|
|
|
case OpType.ConstrainedSort:
|
|
|
|
return "ConstrainedSort";
|
|
|
|
default:
|
|
|
|
Debug.Assert(false, "need to special case enum->string: " + op.ToString());
|
|
|
|
return op.ToString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
internal struct AutoXml : IDisposable {
|
|
|
|
private string _nodeName;
|
|
|
|
private Dump _dumper;
|
|
|
|
|
|
|
|
internal AutoXml(Dump dumper, Op op) {
|
|
|
|
_dumper = dumper;
|
|
|
|
_nodeName = AutoString.ToString(op.OpType);
|
|
|
|
|
|
|
|
Dictionary<string, object> attrs = new Dictionary<string, object>();
|
|
|
|
if (null != op.Type) {
|
|
|
|
attrs.Add("Type", TypeHelpers.GetFullName(op.Type));
|
|
|
|
}
|
|
|
|
|
|
|
|
_dumper.Begin(_nodeName, attrs);
|
|
|
|
}
|
|
|
|
|
|
|
|
internal AutoXml(Dump dumper, Op op, Dictionary<string, object> attrs) {
|
|
|
|
_dumper = dumper;
|
|
|
|
_nodeName = AutoString.ToString(op.OpType);
|
|
|
|
|
|
|
|
Dictionary<string, object> attrs2 = new Dictionary<string, object>();
|
|
|
|
if (null != op.Type) {
|
|
|
|
attrs2.Add("Type", TypeHelpers.GetFullName(op.Type));
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach (KeyValuePair<string, object> kv in attrs) {
|
|
|
|
attrs2.Add(kv.Key, kv.Value);
|
|
|
|
}
|
|
|
|
|
|
|
|
_dumper.Begin(_nodeName, attrs2);
|
|
|
|
}
|
|
|
|
|
|
|
|
internal AutoXml(Dump dumper, string nodeName)
|
|
|
|
: this(dumper, nodeName, null) {
|
|
|
|
}
|
|
|
|
|
|
|
|
internal AutoXml(Dump dumper, string nodeName, Dictionary<string, object> attrs) {
|
|
|
|
_dumper = dumper;
|
|
|
|
_nodeName = nodeName;
|
|
|
|
_dumper.Begin(_nodeName, attrs);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void Dispose() {
|
|
|
|
_dumper.End(_nodeName);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|