Skip to content

Commit

Permalink
[QP-6629] Insert 및 Update의 row에 존재하는 subquery에 대한 컬럼 및 테이블 정보 취득 (#84)
Browse files Browse the repository at this point in the history
# 개요
- Row에 서브쿼리가 포함될 경우, 서브쿼리 정보를 `QsiTableStructure`에 담아
`IAnalysisResult`에서 제공합니다.

# 변경사항
## 주요 변경사항
- `ITableStructureCache` 인터페이스를 통해, 키에 해당하는 `QsiTableStructure` 리스트를
반환합니다.
- `QsiDataManipulationResult` 및 `QsiExplainDataManipulationResult`에
`TablesInRows`를 추가합니다.
  - 해당 프로퍼티를 통해 Row에 존재하는 서브쿼리 정보를 전달하게 됩니다.
- `QsiActionAnalyzer`에서 Row 내에 서브쿼리가 존재할 경우, 이를 습득하여
`QsiTableStructure`로 전환하고 이를 결과로 제공합니다.
  - Insert 문의 경우, 모든 Row를 순회하며 서브쿼리를 해당 컨텍스트의 cache에 담습니다.
    - 이후 결과를 반환할 때 cache에 담긴 모든 테이블을 반환합니다.
- Update 문의 경우, 역시 Row를 순회하며 서브쿼리를 임시 리스트에 저장 후 결과 반환 시 리스트의 테이블들을
반환합니다.

## 기타 변경사항
- 추가: 디버거에서 `QsiExplainDanaManipulationResult`의 `TablesInRows` 프로퍼티에
존재하는 테이블들을 볼 수 있도록 기능을 추가했습니다.
- 수정: Expression 내에 존재하는 서브쿼리들을 리스트로 flatten하여 가져오는 CollectSubqueries
함수를 inner function에서 private 메서드로 변경했습니다.

# 스크린샷
<img width="2672" alt="image"
src="https://github.com/chequer-io/qsi/assets/43464986/568c4e40-0d80-4670-a147-5769eeff35e1">

<img width="2672" alt="image"
src="https://github.com/chequer-io/qsi/assets/43464986/738318a8-7f33-4d22-a8b5-f2eb76035e36">


# 이슈
QP-6629
  • Loading branch information
challenger71498 authored Jun 4, 2024
1 parent 59d0f2a commit 95d8ca0
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 25 deletions.
2 changes: 2 additions & 0 deletions Qsi.Debugger/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
using Qsi.Debugger.Vendor.Redshift;
using Qsi.Debugger.Vendor.SqlServer;
using Qsi.Debugger.Vendor.Trino;
using Qsi.Engines.Explain;
using Qsi.SqlServer.Common;
using Qsi.Tree;

Expand Down Expand Up @@ -232,6 +233,7 @@ private async void Update()
{
IQsiAnalysisResult[] results = await _vendor.Engine.Explain(script);
tables.AddRange(results.OfType<QsiTableResult>().Select(r => r.Table));
tables.AddRange(results.OfType<QsiExplainDataManipulationResult>().SelectMany(r => r.TablesInRows));
}

BuildQsiTableTree(tables);
Expand Down
42 changes: 42 additions & 0 deletions Qsi/Analyzers/Action/Cache/DmlTableStructureCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Collections.Generic;
using Qsi.Analyzers.Action.Models;
using Qsi.Data;

namespace Qsi.Analyzers.Action.Cache;

public class DmlTableStructureCache : ITableStructureCache<DataManipulationTarget>
{
private readonly Dictionary<DataManipulationTarget, List<QsiTableStructure>> _cache = new();

public void Add(DataManipulationTarget key, QsiTableStructure table)
{
if (_cache.TryGetValue(key, out List<QsiTableStructure> value))
{
value.Add(table);
return;
}

_cache[key] = new List<QsiTableStructure> { table };
}

public void AddRange(DataManipulationTarget key, IEnumerable<QsiTableStructure> tables)
{
if (_cache.TryGetValue(key, out List<QsiTableStructure> value))
{
value.AddRange(tables);
return;
}

_cache[key] = new List<QsiTableStructure>(tables);
}

public ICollection<QsiTableStructure> Get(DataManipulationTarget key)
{
return _cache[key];
}

public void Clear()
{
_cache.Clear();
}
}
36 changes: 36 additions & 0 deletions Qsi/Analyzers/Action/Cache/ITableStructureCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using Qsi.Data;

namespace Qsi.Analyzers.Action.Cache;

/// <summary>
/// 사용된 서브쿼리를 분석한 Table Structure 리스트를 캐싱하는 인터페이스입니다.
/// </summary>
public interface ITableStructureCache<in T>
{
/// <summary>
/// T에 해당하는 Table Structure를 추가합니다.
/// </summary>
/// <param name="key">캐시를 저장할 키 값입니다.</param>
/// <param name="table">캐싱할 테이블입니다.</param>
void Add(T key, QsiTableStructure table);

/// <summary>
/// T에 해당하는 Table Structure 목록을 추가합니다.
/// </summary>
/// <param name="key">캐시를 저장할 키 값입니다.</param>
/// <param name="tables">캐싱할 테이블 목록입니다.</param>
void AddRange(T key, IEnumerable<QsiTableStructure> tables);

/// <summary>
/// T에 해당하는 Table Structure 목록을 가져옵니다.
/// </summary>
/// <param name="key">목록을 식별할 수 있는 키 값입니다.</param>
/// <returns>Table Structure 목록입니다.</returns>
ICollection<QsiTableStructure> Get(T key);

/// <summary>
/// 캐시를 초기화합니다.
/// </summary>
void Clear();
}
7 changes: 5 additions & 2 deletions Qsi/Analyzers/Action/Context/TableDataInsertContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Qsi.Analyzers.Action.Models;
using Qsi.Analyzers.Action.Cache;
using Qsi.Analyzers.Action.Models;
using Qsi.Analyzers.Context;
using Qsi.Data;

Expand All @@ -10,7 +11,9 @@ public sealed class TableDataInsertContext : TableDataContext

public ColumnTarget[] ColumnTargets { get; set; }

public DmlTableStructureCache TableStructureCache { get; } = new();

public TableDataInsertContext(IAnalyzerContext context, QsiTableStructure table) : base(context, table)
{
}
}
}
121 changes: 105 additions & 16 deletions Qsi/Analyzers/Action/QsiActionAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,23 +188,23 @@ protected virtual QsiDataValue ResolveRawColumnValue(IAnalyzerContext context, I

var value = context.Engine.TreeDeparser.Deparse(expression, context.Script);
return new QsiDataValue(value, QsiDataType.Raw);
}

IEnumerable<IQsiTableExpressionNode> CollectSubqueries(IQsiExpressionNode node)
{
var queue = new Queue<IQsiTreeNode>();
queue.Enqueue(node);
private IEnumerable<IQsiTableExpressionNode> CollectSubqueries(IQsiExpressionNode node)
{
var queue = new Queue<IQsiTreeNode>();
queue.Enqueue(node);

while (queue.TryDequeue(out var item))
while (queue.TryDequeue(out var item))
{
if (item is IQsiTableExpressionNode tNode)
{
if (item is IQsiTableExpressionNode tNode)
{
yield return tNode;
}
else
{
foreach (var child in item.Children ?? Enumerable.Empty<IQsiTreeNode>())
queue.Enqueue(child);
}
yield return tNode;
}
else
{
foreach (var child in item.Children ?? Enumerable.Empty<IQsiTreeNode>())
queue.Enqueue(child);
}
}
}
Expand Down Expand Up @@ -505,7 +505,8 @@ protected virtual async ValueTask<IQsiAnalysisResult[]> ExecuteDataInsertAction(
Table = t.Table,
AffectedColumns = GetAffectedColumns(t),
InsertRows = t.InsertRows.ToNullIfEmpty(),
DuplicateRows = t.DuplicateRows.ToNullIfEmpty()
DuplicateRows = t.DuplicateRows.ToNullIfEmpty(),
TablesInRows = dataContext.TableStructureCache.Get(t)
})
.ToArray<IQsiAnalysisResult>();
}
Expand Down Expand Up @@ -681,6 +682,31 @@ private async ValueTask ProcessQueryValues(TableDataInsertContext context, IQsiT
{
PopulateInsertRow(context, pivot => row.Items[pivot.SourceOrder]);
}

var tableAnalyzer = context.Engine.GetAnalyzer<QsiTableAnalyzer>();

QsiTableStructure table;

using (var tableContext = new TableCompileContext(context))
table = await tableAnalyzer.BuildTableStructure(tableContext, valueTable);

foreach (var target in context.Targets)
context.TableStructureCache.Add(target, table);

if (directives is null)
return;

QsiTableStructure[] directiveTables = directives.Tables
.Select(t =>
{
using var tableContext = new TableCompileContext(context);
return tableAnalyzer.BuildTableStructure(tableContext, t).Result;
})
.ToArray();

// TODO: 동일한 TableStructure를 서로 다른 키에 계속 저장해야 하는 문제가 있습니다.
foreach (var target in context.Targets)
context.TableStructureCache.AddRange(target, directiveTables);
}

private void ProcessValues(TableDataInsertContext context, IQsiRowValueExpressionNode[] rows)
Expand All @@ -695,6 +721,7 @@ private void ProcessValues(TableDataInsertContext context, IQsiRowValueExpressio
throw new QsiException(QsiError.DifferentColumnValueCount, i + 1);

PopulateInsertRow(context, pivot => ResolveColumnValue(context, row.ColumnValues[pivot.DeclaredColumnTarget.DeclaredOrder]));
StoreTablesFromRow(context, pivot => row.ColumnValues[pivot.DeclaredColumnTarget.DeclaredOrder]);
}
}

Expand All @@ -705,6 +732,12 @@ private void ProcessSetValues(TableDataInsertContext context, IQsiSetColumnExpre
var declaredColumnTarget = (SetColumnTarget)pivot.DeclaredColumnTarget;
return ResolveColumnValue(context, declaredColumnTarget.ValueNode);
});

StoreTablesFromRow(context, pivot =>
{
var declaredColumnTarget = (SetColumnTarget)pivot.DeclaredColumnTarget;
return declaredColumnTarget.ValueNode;
});
}

// TODO: action.Target, action.Condition
Expand Down Expand Up @@ -765,6 +798,25 @@ private void PopulateInsertRow(TableDataInsertContext context, DataValueSelector
target.InsertRows.Add(targetRow);
}
}

private void StoreTablesFromRow(TableDataInsertContext context, Func<DataManipulationTargetDataPivot, IQsiExpressionNode> expressionSelector)
{
var tableAnalyzer = context.Engine.GetAnalyzer<QsiTableAnalyzer>();

foreach (var target in context.Targets)
{
IEnumerable<QsiTableStructure> tables = target.DataPivots
.Select(expressionSelector)
.SelectMany(CollectSubqueries)
.Select(n =>
{
using var tableContext = new TableCompileContext(context);
return tableAnalyzer.BuildTableStructure(tableContext, n.Table).Result;
});

context.TableStructureCache.AddRange(target, tables);
}
}
#endregion

#region Delete
Expand Down Expand Up @@ -896,6 +948,11 @@ protected virtual async ValueTask<IQsiAnalysisResult[]> ExecuteDataUpdateAction(
var affectedColumnMap = new bool[sourceTable.Columns.Count];
ColumnTarget[] columnTargets;

var tablesInRows = new IEnumerable<QsiTableStructure>[sourceTable.Columns.Count];

for (var i = 0; i < sourceTable.Columns.Count; ++i)
tablesInRows[i] = Enumerable.Empty<QsiTableStructure>();

if (!ListUtility.IsNullOrEmpty(action.SetValues))
{
SetColumnTarget[] setColumnTargets = ResolveSetColumnTargets(context, sourceTable, action.SetValues);
Expand All @@ -906,6 +963,8 @@ protected virtual async ValueTask<IQsiAnalysisResult[]> ExecuteDataUpdateAction(
var targetIndex = columnTarget.SourceColumn.Parent.Columns.IndexOf(columnTarget.SourceColumn);
values[targetIndex] = ResolveColumnValue(context, columnTarget.ValueNode);
affectedColumnMap[targetIndex] = true;

tablesInRows[targetIndex] = tablesInRows[targetIndex].Concat(GetTables(columnTarget.ValueNode));
}
}
else if (action.Value != null)
Expand All @@ -916,7 +975,10 @@ protected virtual async ValueTask<IQsiAnalysisResult[]> ExecuteDataUpdateAction(
throw new QsiException(QsiError.DifferentColumnValueCount, 0);

for (int i = 0; i < values.Length; i++)
{
values[i] = ResolveColumnValue(context, action.Value.ColumnValues[i]);
tablesInRows[i] = GetTables(action.Value.ColumnValues[i]);
}

affectedColumnMap.AsSpan().Fill(true);
}
Expand Down Expand Up @@ -966,10 +1028,37 @@ protected virtual async ValueTask<IQsiAnalysisResult[]> ExecuteDataUpdateAction(
Table = target.Table,
AffectedColumns = affectedColumns,
UpdateBeforeRows = target.UpdateBeforeRows.ToNullIfEmpty(),
UpdateAfterRows = target.UpdateAfterRows.ToNullIfEmpty()
UpdateAfterRows = target.UpdateAfterRows.ToNullIfEmpty(),
TablesInRows = FilterTablesInRows(affectedColumnMap).ToArray()
};
})
.ToArray<IQsiAnalysisResult>();

IEnumerable<QsiTableStructure> GetTables(IQsiExpressionNode expression)
{
IEnumerable<IQsiTableExpressionNode> subqueries = CollectSubqueries(expression);

if (!subqueries.Any())
return Enumerable.Empty<QsiTableStructure>();

return subqueries.Select(q =>
{
using var ctx = new TableCompileContext(context);
return tableAnalyzer.BuildTableStructure(ctx, q.Table).Result;
});
}

IEnumerable<QsiTableStructure> FilterTablesInRows(bool[] flagMap)
{
for (var i = 0; i < tablesInRows.Length; ++i)
{
if (!flagMap[i])
continue;

foreach (var item in tablesInRows[i])
yield return item;
}
}
}

protected int FindColumnIndex(IAnalyzerContext context, QsiTableStructure table, QsiQualifiedIdentifier identifier)
Expand Down
8 changes: 6 additions & 2 deletions Qsi/Data/Action/QsiDataManipulationResult.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Qsi.Analyzers;
using System;
using System.Collections.Generic;
using Qsi.Analyzers;

namespace Qsi.Data;

Expand All @@ -18,5 +20,7 @@ public class QsiDataManipulationResult : IQsiAnalysisResult

public QsiDataRowCollection DeleteRows { get; set; }

public ICollection<QsiTableStructure> TablesInRows { get; set; } = Array.Empty<QsiTableStructure>();

public QsiSensitiveDataCollection SensitiveDataCollection { get; } = new();
}
}
5 changes: 3 additions & 2 deletions Qsi/Engines/Explain/Analyzers/ExplainActionAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,12 @@ private QsiExplainDataManipulationResult ConvertToExplain(QsiDataManipulationRes
var explainResult = new QsiExplainDataManipulationResult(
result.Table,
result.AffectedColumns,
operations
operations,
result.TablesInRows
);

explainResult.SensitiveDataCollection.AddRange(result.SensitiveDataCollection);

return explainResult;
}
}
}
23 changes: 20 additions & 3 deletions Qsi/Engines/Explain/Results/QsiExplainDataManipulationResult.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Qsi.Analyzers;
using System;
using System.Collections.Generic;
using Qsi.Analyzers;
using Qsi.Data;

namespace Qsi.Engines.Explain;
Expand All @@ -13,10 +15,25 @@ public sealed class QsiExplainDataManipulationResult : IQsiAnalysisResult

public QsiSensitiveDataCollection SensitiveDataCollection { get; } = new();

public QsiExplainDataManipulationResult(QsiTableStructure table, QsiTableColumn[] affectedColumns, QsiDataValueOperation[] operations)
public ICollection<QsiTableStructure> TablesInRows { get; }

public QsiExplainDataManipulationResult(
QsiTableStructure table,
QsiTableColumn[] affectedColumns,
QsiDataValueOperation[] operations)
: this(table, affectedColumns, operations, Array.Empty<QsiTableStructure>())
{
}

public QsiExplainDataManipulationResult(
QsiTableStructure table,
QsiTableColumn[] affectedColumns,
QsiDataValueOperation[] operations,
ICollection<QsiTableStructure> tablesInRows)
{
Table = table;
AffectedColumns = affectedColumns;
Operations = operations;
TablesInRows = tablesInRows;
}
}
}

0 comments on commit 95d8ca0

Please sign in to comment.