Migrate remaining legacy operator workflows
This commit is contained in:
133
tools/MBN_STOCK_WEBVIEW.DbSmoke/NxtThemeRestoreDbAudit.cs
Normal file
133
tools/MBN_STOCK_WEBVIEW.DbSmoke/NxtThemeRestoreDbAudit.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
internal static class NxtThemeRestoreDbAudit
|
||||
{
|
||||
private const string ActiveRowsQueryName = "AUDIT_ACTIVE_NXT_THEME_ROWS";
|
||||
|
||||
private const string ActiveRowsSql = """
|
||||
SELECT LIST_TEXT
|
||||
FROM (
|
||||
SELECT p.LIST_TEXT
|
||||
FROM DC_LIST d
|
||||
JOIN PLAY_LIST p ON p.PG_CODE = d.DC_CODE
|
||||
WHERE p.LIST_TEXT LIKE '1^테마_NXT^%'
|
||||
ORDER BY d.DC_CODE, TO_NUMBER(p.ITEM_INDEX)
|
||||
)
|
||||
WHERE ROWNUM <= :row_limit
|
||||
""";
|
||||
|
||||
internal static async Task RunAsync(
|
||||
IDataQueryExecutor executor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const int maximumRows = 1_000;
|
||||
var spec = new DataQuerySpec(
|
||||
ActiveRowsSql,
|
||||
[new DataQueryParameter("row_limit", maximumRows + 1, DbType.Int32)]);
|
||||
spec.ValidateFor(DataSourceKind.Oracle);
|
||||
var table = await executor.ExecuteAsync(
|
||||
DataSourceKind.Oracle,
|
||||
ActiveRowsQueryName,
|
||||
spec,
|
||||
cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (table.Columns.Count != 1 ||
|
||||
table.Columns[0].ColumnName != "LIST_TEXT" ||
|
||||
table.Columns[0].DataType != typeof(string) ||
|
||||
table.Rows.Count > maximumRows)
|
||||
{
|
||||
throw new InvalidDataException("The active NXT-theme audit boundary is invalid.");
|
||||
}
|
||||
|
||||
var rows = new List<LegacyNamedNxtThemeRestoreRow>(table.Rows.Count);
|
||||
var grammar = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
foreach (DataRow dataRow in table.Rows)
|
||||
{
|
||||
if (dataRow[0] is not string text)
|
||||
{
|
||||
throw new InvalidDataException("An active NXT-theme row is not text.");
|
||||
}
|
||||
|
||||
var fields = text.Split('^');
|
||||
if (fields.Length != 7 || fields[0] != "1" || fields[1] != "테마_NXT")
|
||||
{
|
||||
throw new InvalidDataException("An active NXT-theme row has an invalid shape.");
|
||||
}
|
||||
|
||||
var pageText = fields[5].Replace(" ", string.Empty, StringComparison.Ordinal);
|
||||
rows.Add(new LegacyNamedNxtThemeRestoreRow(
|
||||
rows.Count,
|
||||
IsEnabled: true,
|
||||
fields[1],
|
||||
fields[2],
|
||||
fields[3],
|
||||
fields[4],
|
||||
pageText,
|
||||
fields[6]));
|
||||
var key = string.Join('|', fields[3], fields[4]);
|
||||
grammar[key] = grammar.GetValueOrDefault(key) + 1;
|
||||
}
|
||||
|
||||
if (rows.Count == 0)
|
||||
{
|
||||
Console.WriteLine("NXT_THEME_RESTORE_AUDIT: active=0");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await new LegacyNamedNxtThemeRestoreService(executor)
|
||||
.ResolveAsync(rows, cancellationToken);
|
||||
var resolved = result.Rows.Count(row => row.IsResolved);
|
||||
var identityMapped = result.Rows.Count(row => row.ObservedCurrentThemeCode is not null);
|
||||
var remapped = result.Rows
|
||||
.Select((row, index) =>
|
||||
row.ObservedCurrentThemeCode is not null &&
|
||||
!string.Equals(
|
||||
row.ObservedCurrentThemeCode,
|
||||
rows[index].DataCode,
|
||||
StringComparison.Ordinal))
|
||||
.Count(static value => value);
|
||||
var failures = result.Rows
|
||||
.Where(row => row.Failure.HasValue)
|
||||
.GroupBy(row => row.Failure!.Value)
|
||||
.OrderBy(group => group.Key)
|
||||
.Select(group => $"{group.Key}={group.Count().ToString(CultureInfo.InvariantCulture)}")
|
||||
.ToArray();
|
||||
var actions = result.Rows
|
||||
.Where(row => row.IsResolved)
|
||||
.GroupBy(row => row.Identity!.ActionId)
|
||||
.OrderBy(group => group.Key, StringComparer.Ordinal)
|
||||
.Select(group => $"{group.Key}={group.Count().ToString(CultureInfo.InvariantCulture)}")
|
||||
.ToArray();
|
||||
var liveCounts = result.Rows
|
||||
.Where(row => row.IsResolved)
|
||||
.Select(row => row.Identity!.LiveItemCount)
|
||||
.ToArray();
|
||||
|
||||
Console.WriteLine(
|
||||
"NXT_THEME_RESTORE_AUDIT: " +
|
||||
$"active={rows.Count.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"distinctTitles={rows.Select(row => row.Subject).Distinct(StringComparer.Ordinal).Count().ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"grammarVariants={grammar.Count.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"identityMapped={identityMapped.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"resolved={resolved.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"remapped={remapped.ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"blocked={(rows.Count - resolved).ToString(CultureInfo.InvariantCulture)} " +
|
||||
$"session={LegacyNamedNxtThemeRestoreService.SessionForLocalHour(DateTime.Now.Hour)}");
|
||||
Console.WriteLine("NXT_THEME_RESTORE_GRAMMAR: " + string.Join(",", grammar
|
||||
.OrderBy(pair => pair.Key, StringComparer.Ordinal)
|
||||
.Select(pair => $"{pair.Key}={pair.Value.ToString(CultureInfo.InvariantCulture)}")));
|
||||
Console.WriteLine("NXT_THEME_RESTORE_ACTIONS: " +
|
||||
(actions.Length == 0 ? "none" : string.Join(",", actions)));
|
||||
Console.WriteLine("NXT_THEME_RESTORE_FAILURES: " +
|
||||
(failures.Length == 0 ? "none" : string.Join(",", failures)));
|
||||
Console.WriteLine("NXT_THEME_RESTORE_LIVE_ITEMS: " +
|
||||
(liveCounts.Length == 0
|
||||
? "none"
|
||||
: $"min={liveCounts.Min().ToString(CultureInfo.InvariantCulture)}," +
|
||||
$"max={liveCounts.Max().ToString(CultureInfo.InvariantCulture)}," +
|
||||
$"sum={liveCounts.Sum().ToString(CultureInfo.InvariantCulture)}"));
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,12 @@ try
|
||||
|
||||
await VerifyOracleServerVersionAsync(runtime.Executor, cancellation.Token);
|
||||
await VerifyStockSearchAsync(runtime.Executor, cancellation.Token);
|
||||
await VerifyThemeSelectorAsync(runtime.Executor, cancellation.Token);
|
||||
await VerifyOverseasSelectorsAsync(runtime.Executor, cancellation.Token);
|
||||
await VerifyExpertSelectorAsync(runtime.Executor, cancellation.Token);
|
||||
await VerifyTradingHaltSelectorAsync(runtime.Executor, cancellation.Token);
|
||||
await VerifyOperatorCatalogSchemaAsync(runtime.Executor, cancellation.Token);
|
||||
await NxtThemeRestoreDbAudit.RunAsync(runtime.Executor, cancellation.Token);
|
||||
|
||||
var service = new LegacyMarketDataService(runtime.Executor);
|
||||
await VerifySnapshotAsync(service, MarketDataView.Kospi, cancellation.Token);
|
||||
@@ -247,6 +249,33 @@ static async Task VerifyStockSearchAsync(
|
||||
$"({string.Join(", ", counts)})");
|
||||
}
|
||||
|
||||
static async Task VerifyThemeSelectorAsync(
|
||||
IDataQueryExecutor executor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Console.WriteLine("ThemeSelector: BEGIN");
|
||||
var service = new LegacyThemeSelectionService(executor);
|
||||
var themes = await service.SearchAsync(
|
||||
string.Empty,
|
||||
ThemeSession.PreMarket,
|
||||
maximumResults: 100,
|
||||
cancellationToken);
|
||||
if (themes.Items.Count == 0)
|
||||
{
|
||||
throw new DatabaseConfigurationException(
|
||||
"The production theme selector returned no unambiguous canonical rows.");
|
||||
}
|
||||
|
||||
var selected = themes.Items[0];
|
||||
var preview = await service.PreviewAsync(
|
||||
selected.Identity,
|
||||
maximumItems: 100,
|
||||
cancellationToken);
|
||||
Console.WriteLine(
|
||||
$"ThemeSelector: themes={themes.Items.Count.ToString(CultureInfo.InvariantCulture)}, " +
|
||||
$"preview={preview.Items.Count.ToString(CultureInfo.InvariantCulture)}");
|
||||
}
|
||||
|
||||
static async Task VerifyOverseasSelectorsAsync(
|
||||
IDataQueryExecutor executor,
|
||||
CancellationToken cancellationToken)
|
||||
|
||||
@@ -78,13 +78,68 @@ internal static class RealSceneDbSmoke
|
||||
WHERE ROWNUM = 1
|
||||
""",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var themeIdentity = await SelectStockAsync(
|
||||
traced,
|
||||
"SELECTOR_THEME_IDENTITY",
|
||||
"""
|
||||
SELECT STOCK_CODE, STOCK_NAME
|
||||
FROM (
|
||||
SELECT l.SB_CODE STOCK_CODE, l.SB_TITLE STOCK_NAME
|
||||
FROM SB_LIST l
|
||||
WHERE l.SB_MARKET = 'KRX'
|
||||
AND l.SB_TITLE IS NOT NULL
|
||||
AND l.SB_TITLE = TRIM(l.SB_TITLE)
|
||||
AND LENGTH(l.SB_TITLE) BETWEEN 1 AND 128
|
||||
AND REGEXP_LIKE(l.SB_CODE, '^[0-9]{3,8}$', 'c')
|
||||
AND (SELECT COUNT(*) FROM SB_LIST d
|
||||
WHERE d.SB_MARKET = l.SB_MARKET
|
||||
AND d.SB_TITLE = l.SB_TITLE) = 1
|
||||
AND (SELECT COUNT(*) FROM SB_LIST d
|
||||
WHERE d.SB_MARKET = l.SB_MARKET
|
||||
AND d.SB_CODE = l.SB_CODE) = 1
|
||||
AND EXISTS (SELECT 1 FROM SB_ITEM i WHERE i.SB_M_CODE = l.SB_CODE)
|
||||
ORDER BY l.SB_CODE
|
||||
)
|
||||
WHERE ROWNUM = 1
|
||||
""",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var nxtThemeIdentity = await SelectIdentityAsync(
|
||||
traced,
|
||||
DataSourceKind.MariaDb,
|
||||
"SELECTOR_NXT_THEME_IDENTITY",
|
||||
"""
|
||||
SELECT l.SB_CODE STOCK_CODE, l.SB_TITLE STOCK_NAME
|
||||
FROM SB_LIST l
|
||||
WHERE l.SB_MARKET = 'NXT'
|
||||
AND l.SB_TITLE IS NOT NULL
|
||||
AND BINARY l.SB_TITLE = BINARY TRIM(l.SB_TITLE)
|
||||
AND CHAR_LENGTH(l.SB_TITLE) BETWEEN 1 AND 128
|
||||
AND l.SB_CODE REGEXP BINARY '^[0-9]{3,8}$'
|
||||
AND (SELECT COUNT(*) FROM SB_LIST d
|
||||
WHERE d.SB_MARKET = l.SB_MARKET
|
||||
AND BINARY d.SB_TITLE = BINARY l.SB_TITLE) = 1
|
||||
AND (SELECT COUNT(*) FROM SB_LIST d
|
||||
WHERE d.SB_MARKET = l.SB_MARKET
|
||||
AND BINARY d.SB_CODE = BINARY l.SB_CODE) = 1
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM SB_ITEM i
|
||||
JOIN v_all_stock b
|
||||
ON SUBSTRING(i.SB_I_CODE, 2, 12) = b.f_stock_code
|
||||
WHERE i.SB_M_CODE = l.SB_CODE
|
||||
AND b.f_stop_gubun = 'N'
|
||||
AND b.f_curr_price != 0)
|
||||
ORDER BY l.SB_CODE
|
||||
LIMIT 1
|
||||
""",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var comparisonNames = await SelectManyAsync(
|
||||
traced,
|
||||
"SELECTOR_COMPARISON_STOCKS",
|
||||
"""
|
||||
SELECT STOCK_NAME
|
||||
SELECT STOCK_CODE || CHR(31) || STOCK_NAME STOCK_IDENTITY
|
||||
FROM (
|
||||
SELECT b.f_stock_wanname STOCK_NAME
|
||||
SELECT b.f_stock_code STOCK_CODE, b.f_stock_wanname STOCK_NAME
|
||||
FROM t_online1 a
|
||||
JOIN t_stock b ON a.f_stock_code = b.f_stock_code
|
||||
WHERE a.f_curr_price <> 0
|
||||
@@ -98,7 +153,7 @@ internal static class RealSceneDbSmoke
|
||||
)
|
||||
WHERE ROWNUM <= 2
|
||||
""",
|
||||
"STOCK_NAME",
|
||||
"STOCK_IDENTITY",
|
||||
maximumRows: 2,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var dealerStockName = await SelectOneAsync(
|
||||
@@ -261,12 +316,8 @@ internal static class RealSceneDbSmoke
|
||||
() => new S5026SceneDataLoader(traced).LoadAsync(
|
||||
new ComparisonPairSceneLoadRequest(
|
||||
ComparisonGraphPeriod.FiveDays,
|
||||
new ComparisonStockSelection(
|
||||
ComparisonEquityMarket.Kospi,
|
||||
RequireComparisonSelector(comparisonNames, 0)),
|
||||
new ComparisonStockSelection(
|
||||
ComparisonEquityMarket.Kospi,
|
||||
RequireComparisonSelector(comparisonNames, 1))),
|
||||
RequireComparisonSelector(comparisonNames, 0),
|
||||
RequireComparisonSelector(comparisonNames, 1)),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5029",
|
||||
@@ -274,12 +325,8 @@ internal static class RealSceneDbSmoke
|
||||
() => new S5029SceneDataLoader(traced).LoadAsync(
|
||||
new ComparisonPairSceneLoadRequest(
|
||||
ComparisonGraphPeriod.FiveDays,
|
||||
new ComparisonStockSelection(
|
||||
ComparisonEquityMarket.Kospi,
|
||||
RequireComparisonSelector(comparisonNames, 0)),
|
||||
new ComparisonStockSelection(
|
||||
ComparisonEquityMarket.Kospi,
|
||||
RequireComparisonSelector(comparisonNames, 1))),
|
||||
RequireComparisonSelector(comparisonNames, 0),
|
||||
RequireComparisonSelector(comparisonNames, 1)),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5032",
|
||||
@@ -301,6 +348,36 @@ internal static class RealSceneDbSmoke
|
||||
() => new S5074SceneDataLoader(traced).LoadAsync(
|
||||
pageRequest,
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5074",
|
||||
"Oracle/theme-code-title-identity",
|
||||
() => new S5074SceneDataLoader(traced).LoadAsync(
|
||||
new ThemePagedQuoteLoadRequest(
|
||||
RequireSelector(themeIdentity?.Code, "theme code"),
|
||||
RequireSelector(themeIdentity?.Name, "theme title"),
|
||||
PagedQuoteThemeSort.InputOrder,
|
||||
PagedQuoteThemeValueKind.Current),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
if (nxtThemeIdentity is null)
|
||||
{
|
||||
Console.WriteLine(
|
||||
"SCENE_SMOKE:s5074:PREREQUISITE - no NXT theme currently has an eligible live quote");
|
||||
}
|
||||
else
|
||||
{
|
||||
await run.RequiredAsync(
|
||||
"s5074",
|
||||
"MariaDb/NXT-theme-code-title-identity",
|
||||
() => new S5074SceneDataLoader(traced).LoadAsync(
|
||||
new NxtThemePagedQuoteLoadRequest(
|
||||
nxtThemeIdentity.Code,
|
||||
nxtThemeIdentity.Name + "(NXT)",
|
||||
PagedQuoteThemeSort.InputOrder,
|
||||
DateTime.Now.Hour <= 12
|
||||
? NxtMarketSession.PreMarket
|
||||
: NxtMarketSession.AfterMarket),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
}
|
||||
await run.RequiredAsync(
|
||||
"s5076",
|
||||
"Oracle/manual-pie",
|
||||
@@ -379,12 +456,8 @@ internal static class RealSceneDbSmoke
|
||||
() => new S5087SceneDataLoader(traced).LoadAsync(
|
||||
new ComparisonPairSceneLoadRequest(
|
||||
ComparisonGraphPeriod.FiveDays,
|
||||
new ComparisonStockSelection(
|
||||
ComparisonEquityMarket.Kospi,
|
||||
RequireComparisonSelector(comparisonNames, 0)),
|
||||
new ComparisonStockSelection(
|
||||
ComparisonEquityMarket.Kospi,
|
||||
RequireComparisonSelector(comparisonNames, 1))),
|
||||
RequireComparisonSelector(comparisonNames, 0),
|
||||
RequireComparisonSelector(comparisonNames, 1)),
|
||||
cancellationToken)).ConfigureAwait(false);
|
||||
await run.RequiredAsync(
|
||||
"s5088",
|
||||
@@ -505,12 +578,25 @@ internal static class RealSceneDbSmoke
|
||||
IDataQueryExecutor executor,
|
||||
string tableName,
|
||||
string sql,
|
||||
CancellationToken cancellationToken) =>
|
||||
await SelectIdentityAsync(
|
||||
executor,
|
||||
DataSourceKind.Oracle,
|
||||
tableName,
|
||||
sql,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
private static async Task<StockSelector?> SelectIdentityAsync(
|
||||
IDataQueryExecutor executor,
|
||||
DataSourceKind source,
|
||||
string tableName,
|
||||
string sql,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var table = await executor.ExecuteAsync(
|
||||
DataSourceKind.Oracle,
|
||||
source,
|
||||
tableName,
|
||||
new DataQuerySpec(sql),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
@@ -599,16 +685,25 @@ internal static class RealSceneDbSmoke
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string RequireComparisonSelector(
|
||||
private static ComparisonStockSelection RequireComparisonSelector(
|
||||
IReadOnlyList<string> values,
|
||||
int index)
|
||||
{
|
||||
if (values.Count != 2 || index is < 0 or > 1)
|
||||
{
|
||||
return RequireSelector(null, "comparison stock pair");
|
||||
_ = RequireSelector(null, "comparison stock pair");
|
||||
}
|
||||
|
||||
return values[index];
|
||||
var parts = values[index].Split('\u001f');
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
_ = RequireSelector(null, "comparison stock identity");
|
||||
}
|
||||
|
||||
return new ComparisonStockSelection(
|
||||
ComparisonEquityMarket.Kospi,
|
||||
parts[1],
|
||||
parts[0]);
|
||||
}
|
||||
|
||||
private static string SafeFailure(Exception exception) => exception switch
|
||||
|
||||
Reference in New Issue
Block a user