feat: establish legacy parity migration baseline
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
|
||||
/// <summary>
|
||||
/// Compatibility search for the operator screen. Unlike the modern lookup it intentionally
|
||||
/// keeps blank-enter, LIKE wildcard, provider ordering and unlimited-result semantics.
|
||||
/// Values remain bound parameters so parity does not reintroduce SQL concatenation.
|
||||
/// </summary>
|
||||
public sealed class LegacyParityStockSearchService : ILegacyStockLookup
|
||||
{
|
||||
public const int LegacyTextBoxMaximumLength = 32_767;
|
||||
|
||||
private static readonly SearchProfile[] Profiles =
|
||||
[
|
||||
new(
|
||||
LegacyDomesticStockMarket.Kospi,
|
||||
DataSourceKind.Oracle,
|
||||
"LEGACY_PARITY_KOSPI",
|
||||
"SELECT F_STOCK_WANNAME AS STOCK_NAME, F_STOCK_CODE AS STOCK_CODE FROM T_STOCK WHERE F_MKT_HALT = 'N' AND UPPER(F_STOCK_WANNAME) LIKE '%' || :search_term || '%'",
|
||||
"SELECT F_STOCK_WANNAME AS STOCK_NAME, F_STOCK_CODE AS STOCK_CODE FROM T_STOCK WHERE F_MKT_HALT = 'N'"),
|
||||
new(
|
||||
LegacyDomesticStockMarket.Kosdaq,
|
||||
DataSourceKind.Oracle,
|
||||
"LEGACY_PARITY_KOSDAQ",
|
||||
"SELECT F_STOCK_WANNAME AS STOCK_NAME, F_STOCK_CODE AS STOCK_CODE FROM T_KOSDAQ_STOCK WHERE F_MKT_HALT = 'N' AND UPPER(F_STOCK_WANNAME) LIKE '%' || :search_term || '%'",
|
||||
"SELECT F_STOCK_WANNAME AS STOCK_NAME, F_STOCK_CODE AS STOCK_CODE FROM T_KOSDAQ_STOCK WHERE F_MKT_HALT = 'N'"),
|
||||
new(
|
||||
LegacyDomesticStockMarket.NxtKospi,
|
||||
DataSourceKind.MariaDb,
|
||||
"LEGACY_PARITY_NXT_KOSPI",
|
||||
"SELECT CONCAT(a.F_STOCK_NAME, '(NXT)') AS STOCK_NAME, a.F_STOCK_CODE AS STOCK_CODE FROM N_STOCK a, N_ONLINE b WHERE a.F_STOP_GUBUN = 'N' AND a.F_STOCK_CODE = b.F_STOCK_CODE AND UPPER(a.F_STOCK_NAME) LIKE CONCAT('%', @search_term, '%')",
|
||||
"SELECT CONCAT(F_STOCK_NAME, '(NXT)') AS STOCK_NAME, F_STOCK_CODE AS STOCK_CODE FROM N_STOCK WHERE F_STOP_GUBUN = 'N'"),
|
||||
new(
|
||||
LegacyDomesticStockMarket.NxtKosdaq,
|
||||
DataSourceKind.MariaDb,
|
||||
"LEGACY_PARITY_NXT_KOSDAQ",
|
||||
"SELECT CONCAT(a.F_STOCK_NAME, '(NXT)') AS STOCK_NAME, a.F_STOCK_CODE AS STOCK_CODE FROM N_KOSDAQ_STOCK a, N_KOSDAQ_ONLINE b WHERE a.F_STOP_GUBUN = 'N' AND a.F_STOCK_CODE = b.F_STOCK_CODE AND UPPER(a.F_STOCK_NAME) LIKE CONCAT('%', @search_term, '%')",
|
||||
"SELECT CONCAT(F_STOCK_NAME, '(NXT)') AS STOCK_NAME, F_STOCK_CODE AS STOCK_CODE FROM N_KOSDAQ_STOCK WHERE F_STOP_GUBUN = 'N'")
|
||||
];
|
||||
|
||||
private readonly IDataQueryExecutor _executor;
|
||||
private readonly SemaphoreSlim _masterLoadGate = new(1, 1);
|
||||
private IReadOnlyList<LegacyStockIdentity>? _masterCatalog;
|
||||
|
||||
public LegacyParityStockSearchService(IDataQueryExecutor executor)
|
||||
{
|
||||
_executor = executor ?? throw new ArgumentNullException(nameof(executor));
|
||||
}
|
||||
|
||||
public async Task<LegacyStockSearchData> SearchAsync(
|
||||
string query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
if (query.Length > LegacyTextBoxMaximumLength)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(query),
|
||||
$"The legacy search text cannot exceed {LegacyTextBoxMaximumLength} characters.");
|
||||
}
|
||||
|
||||
var masterCatalog = await GetMasterCatalogAsync(cancellationToken).ConfigureAwait(false);
|
||||
var searchTerm = query.ToUpper(CultureInfo.CurrentCulture);
|
||||
var names = new List<string>();
|
||||
|
||||
foreach (var profile in Profiles)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var parameter = new DataQueryParameter("search_term", searchTerm, DbType.String);
|
||||
var spec = new DataQuerySpec(profile.SearchSql, [parameter]);
|
||||
var table = await _executor
|
||||
.ExecuteAsync(profile.Source, profile.SearchTableName, spec, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
AppendNames(table, profile.SearchTableName, names);
|
||||
}
|
||||
|
||||
return new LegacyStockSearchData(query, names, masterCatalog);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<LegacyStockIdentity>> GetMasterCatalogAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cached = Volatile.Read(ref _masterCatalog);
|
||||
if (cached is not null)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
await _masterLoadGate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
cached = Volatile.Read(ref _masterCatalog);
|
||||
if (cached is not null)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
var items = new List<LegacyStockIdentity>();
|
||||
foreach (var profile in Profiles)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var table = await _executor
|
||||
.ExecuteAsync(
|
||||
profile.Source,
|
||||
$"{profile.SearchTableName}_MASTER",
|
||||
new DataQuerySpec(profile.MasterSql),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
AppendMaster(table, profile, items);
|
||||
}
|
||||
|
||||
cached = Array.AsReadOnly(items.ToArray());
|
||||
Volatile.Write(ref _masterCatalog, cached);
|
||||
return cached;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_masterLoadGate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendNames(DataTable? table, string sourceName, ICollection<string> target)
|
||||
{
|
||||
ValidateTable(table, sourceName);
|
||||
foreach (DataRow row in table!.Rows)
|
||||
{
|
||||
target.Add(Convert.ToString(row[0], CultureInfo.CurrentCulture) ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendMaster(
|
||||
DataTable? table,
|
||||
SearchProfile profile,
|
||||
ICollection<LegacyStockIdentity> target)
|
||||
{
|
||||
ValidateTable(table, $"{profile.SearchTableName}_MASTER");
|
||||
foreach (DataRow row in table!.Rows)
|
||||
{
|
||||
target.Add(new LegacyStockIdentity(
|
||||
profile.Market,
|
||||
Convert.ToString(row[0], CultureInfo.CurrentCulture) ?? string.Empty,
|
||||
Convert.ToString(row[1], CultureInfo.CurrentCulture) ?? string.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateTable(DataTable? table, string sourceName)
|
||||
{
|
||||
if (table is null || table.Columns.Count < 2)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Legacy stock data from {sourceName} does not contain two columns.");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record SearchProfile(
|
||||
LegacyDomesticStockMarket Market,
|
||||
DataSourceKind Source,
|
||||
string SearchTableName,
|
||||
string SearchSql,
|
||||
string MasterSql);
|
||||
}
|
||||
Reference in New Issue
Block a user