175 lines
7.3 KiB
C#
175 lines
7.3 KiB
C#
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 isInitialSearch = HangulSearchMatcher.IsInitialConsonantQuery(query);
|
|
// Keep the candidate set identical to the legacy provider searches.
|
|
// In particular, NXT search joins the online tables while the complete
|
|
// identity master intentionally does not. The master must therefore
|
|
// never be used as the visible result set for an initial-only query.
|
|
var searchTerm = isInitialSearch
|
|
? string.Empty
|
|
: 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);
|
|
}
|
|
|
|
var displayNames = isInitialSearch
|
|
? names.Where(name => HangulSearchMatcher.IsMatch(name, query))
|
|
: names;
|
|
return new LegacyStockSearchData(query, displayNames, 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);
|
|
}
|