쿼리 편집기 고도화 — 자동완성·검증 실행·편집 기본기 (Oracle)
색칠만으로는 편집기라 하기 어렵다. 실제로 쿼리를 칠 때 손이 가는 곳을 메웠다.
■ 자동완성 (Ctrl+Space / 치는 대로)
문맥을 보고 후보를 바꾼다(SqlCompletion — Core 순수 함수라 테스트로 고정했다):
· << 직후 → 치환 변수 51종. 이 편집기에만 있는 문법이라 외워서 칠 수 없다.
고르면 알맹이를 넣고 닫는 >> 를 붙여 준다.
· FROM/JOIN 뒤 → 테이블·뷰 이름(운영 스키마에서 2,494건)
· 별칭. 뒤 → 그 테이블의 컬럼. 별칭은 같은 쿼리 FROM 절에서 푼다
(FROM P_PatMst p → p. 는 P_PatMst 의 컬럼).
· 그 밖 → 예약어·함수·테이블
일반 위치에서 한 글자도 안 쳤으면 목록을 띄우지 않는다 — 아무 데서나 뜨면 방해가 된다.
테이블 목록은 처음 한 번만 읽고, 컬럼은 테이블별로 캐시한다(매번 조회하면 입력이 끊긴다).
DB 가 없으면 예약어·함수·변수만 나오고 나머지는 조용히 빠진다.
별칭 자리에 예약어가 오면 별칭으로 보지 않는다 — FROM P_PatMst WHERE 에서 WHERE 를
별칭으로 잡으면 엉뚱한 테이블의 컬럼을 제안하게 된다.
■ 검증 실행 (F5)
저장 전에 실제로 돌려 본다. 치환 변수는 디자인 시점에 값이 없으므로 런타임과 같은 방식으로
빈 문자열로 바꾼 뒤 실행한다(clsMDataTable.vb:67-71). 목적은 데이터 확인이 아니라
테이블·컬럼 오타와 구문 오류를 배포 전에 잡는 것이다 — 지금까지는 EMR 런타임에서야
처음 실행됐고 실패해도 빈 catch 가 삼켰다.
성공하면 컬럼 목록과 표본 행(최대 20)을 보여 주고, 실패하면 ORA 메시지를 그대로 띄운다.
안전 장치: SELECT/WITH 로 시작하는 조회만 실행한다. 운영 DB 에 붙을 수 있는 도구이고
편집 중인 텍스트를 그대로 실행하므로 DML·DDL 이 섞이면 되돌릴 수 없다.
앞쪽 주석을 걷어내고 판정하므로 /* SELECT */ DELETE ... 같은 우회도 막힌다.
--db-trial 진단을 신설해 실DB 로 확인했다(10/10 통과):
정상 조회 통과 · 없는 테이블 ORA-00942 · 없는 컬럼 ORA-00904 · 구문 오류 ORA-00936 ·
앞 주석 뒤 정상 통과 · INSERT/DELETE/DROP/주석우회 전부 차단 · 빈 쿼리 차단.
■ 편집 기본기
따옴표·괄호 자동 닫기(선택이 있으면 그 선택을 감싼다) — SQL 에서 짝을 빠뜨리면
런타임에서 조용히 실패하는 종류다.
앞서 넣은 줄번호·Tab 들여쓰기·Enter 자동 들여쓰기·Ctrl+/ 주석 토글은 그대로.
단위 테스트 15건 추가(문맥 판정 6종·별칭 풀기 4종·키워드 별칭 배제·후보 정렬·내장 목록).
회귀: 테스트 211/211, 편집 스모크 실패 0, 검증 실행 점검 10/10,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
864f11b0e4
commit
79db31cb4d
@@ -0,0 +1,137 @@
|
||||
using SheetMe.Core.Catalog;
|
||||
|
||||
namespace SheetMe.Core.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 자동완성 문맥 판단 테스트.
|
||||
///
|
||||
/// 문맥을 잘못 잡으면 목록이 엉뚱한 것을 보여 주고, 그러면 안 쓰느니만 못하다.
|
||||
/// 특히 <c><<</c> 직후는 이 편집기에만 있는 문법이라 여기서 놓치면 손으로 외워 쳐야 한다.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public sealed class SqlCompletionTests
|
||||
{
|
||||
/// <summary>커서를 | 로 표시한 텍스트를 받아 분석한다 — 테스트가 읽히게</summary>
|
||||
private static SqlCompletionQuery? At(string marked)
|
||||
{
|
||||
var caret = marked.IndexOf('|');
|
||||
return SqlCompletion.Analyze(marked.Remove(caret, 1), caret);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Analyze_AfterOpenAngle_SuggestsVariables()
|
||||
{
|
||||
var query = At("WHERE a = <<M.CMM|");
|
||||
|
||||
Assert.IsNotNull(query);
|
||||
Assert.AreEqual(SqlCompletionContext.Variable, query.Context);
|
||||
Assert.AreEqual("M.CMM", query.Prefix);
|
||||
}
|
||||
|
||||
/// <summary>이미 닫힌 변수 뒤에서는 변수 문맥이 아니다</summary>
|
||||
[TestMethod]
|
||||
public void Analyze_AfterClosedVariable_IsNotVariableContext()
|
||||
{
|
||||
var query = At("WHERE a = <<A.B.C>> AND na|");
|
||||
|
||||
Assert.IsNotNull(query);
|
||||
Assert.AreNotEqual(SqlCompletionContext.Variable, query.Context);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("SELECT * FROM P_|")]
|
||||
[DataRow("SELECT * FROM t JOIN P_|")]
|
||||
public void Analyze_AfterFromOrJoin_SuggestsTables(string marked)
|
||||
{
|
||||
var query = At(marked);
|
||||
|
||||
Assert.IsNotNull(query);
|
||||
Assert.AreEqual(SqlCompletionContext.TableName, query.Context);
|
||||
Assert.AreEqual("P_", query.Prefix);
|
||||
}
|
||||
|
||||
/// <summary>FROM 바로 뒤(아직 한 글자도 안 침)에서도 테이블을 제안해야 한다</summary>
|
||||
[TestMethod]
|
||||
public void Analyze_ImmediatelyAfterFrom_SuggestsTablesWithEmptyPrefix()
|
||||
{
|
||||
var query = At("SELECT * FROM |");
|
||||
|
||||
Assert.IsNotNull(query);
|
||||
Assert.AreEqual(SqlCompletionContext.TableName, query.Context);
|
||||
Assert.AreEqual(string.Empty, query.Prefix);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Analyze_AfterDot_SuggestsColumnsOfQualifier()
|
||||
{
|
||||
var query = At("SELECT p.Cht| FROM P_PatMst p");
|
||||
|
||||
Assert.IsNotNull(query);
|
||||
Assert.AreEqual(SqlCompletionContext.ColumnOf, query.Context);
|
||||
Assert.AreEqual("p", query.Qualifier);
|
||||
Assert.AreEqual("Cht", query.Prefix);
|
||||
}
|
||||
|
||||
/// <summary>아무것도 안 친 일반 위치에서는 목록을 띄우지 않는다 — 방해가 된다</summary>
|
||||
[TestMethod]
|
||||
public void Analyze_EmptyPrefixInGeneralPosition_ReturnsNull()
|
||||
=> Assert.IsNull(At("SELECT a, |"));
|
||||
|
||||
[TestMethod]
|
||||
public void Analyze_TypingWord_SuggestsGeneral()
|
||||
{
|
||||
var query = At("SEL|");
|
||||
|
||||
Assert.IsNotNull(query);
|
||||
Assert.AreEqual(SqlCompletionContext.General, query.Context);
|
||||
Assert.AreEqual("SEL", query.Prefix);
|
||||
Assert.AreEqual(0, query.ReplaceStart);
|
||||
}
|
||||
|
||||
/// <summary>별칭을 실제 테이블로 풀어야 컬럼을 찾을 수 있다</summary>
|
||||
[DataTestMethod]
|
||||
[DataRow("SELECT p.x FROM P_PatMst p", "p", "P_PatMst")]
|
||||
[DataRow("SELECT p.x FROM P_PatMst AS p", "p", "P_PatMst")]
|
||||
[DataRow("SELECT c.x FROM P_PatMst p JOIN P_ComInf c ON p.a=c.a", "c", "P_ComInf")]
|
||||
public void ResolveTable_FindsAliasTarget(string sql, string qualifier, string expected)
|
||||
=> Assert.AreEqual(expected, SqlCompletion.ResolveTable(sql, qualifier));
|
||||
|
||||
/// <summary>별칭이 없으면 한정자 자체가 테이블 이름이다</summary>
|
||||
[TestMethod]
|
||||
public void ResolveTable_FallsBackToQualifier()
|
||||
=> Assert.AreEqual("P_PatMst", SqlCompletion.ResolveTable("SELECT * FROM P_PatMst", "P_PatMst"));
|
||||
|
||||
/// <summary>
|
||||
/// 별칭 자리에 키워드가 오면 별칭이 아니다 — <c>FROM P_PatMst WHERE ...</c> 에서
|
||||
/// WHERE 를 별칭으로 잡으면 엉뚱한 테이블을 가리킨다.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void ResolveTable_IgnoresKeywordAsAlias()
|
||||
=> Assert.AreEqual("WHERE", SqlCompletion.ResolveTable("SELECT * FROM P_PatMst WHERE a=1", "WHERE"));
|
||||
|
||||
[TestMethod]
|
||||
public void Filter_PutsPrefixMatchesFirst()
|
||||
{
|
||||
var items = new[]
|
||||
{
|
||||
new SqlCompletionItem("PrintBackColor", SqlCompletionKind.Column),
|
||||
new SqlCompletionItem("PrintOutPut", SqlCompletionKind.Column),
|
||||
new SqlCompletionItem("XPrint", SqlCompletionKind.Column),
|
||||
};
|
||||
|
||||
var filtered = SqlCompletion.Filter(items, "Print");
|
||||
|
||||
Assert.AreEqual(3, filtered.Count, "부분 일치도 포함해야 한다");
|
||||
Assert.AreEqual("PrintOutPut", filtered[0].Text, "접두 일치 중 짧은 것이 먼저");
|
||||
Assert.AreEqual("XPrint", filtered[^1].Text, "부분 일치는 뒤로");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BuiltIns_CoverKeywordsAndFunctions()
|
||||
{
|
||||
var items = SqlCompletion.BuiltIns();
|
||||
|
||||
Assert.IsTrue(items.Any(i => i.Text == "SELECT" && i.Kind == SqlCompletionKind.Keyword));
|
||||
Assert.IsTrue(items.Any(i => i.Text == "NVL" && i.Kind == SqlCompletionKind.Function));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user