쿼리 편집기 고도화 — 자동완성·검증 실행·편집 기본기 (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:
Msystech
2026-08-13 15:21:08 +09:00
co-authored by Claude Opus 5
parent 864f11b0e4
commit 79db31cb4d
7 changed files with 1021 additions and 7 deletions
@@ -615,6 +615,68 @@ public static class DbSmoke
}
}
/// <summary>
/// 쿼리 검증 실행 점검(--db-trial) — 편집기의 '검증 실행'이 실제 DB 에서 무엇을 돌려주는지.
///
/// 이 기능은 오타를 배포 전에 잡으라고 만든 것이라, 정상 쿼리는 통과하고
/// 틀린 쿼리는 <b>사람이 읽을 수 있는 이유</b>와 함께 실패해야 한다.
/// 조회 외 문장을 막는 안전 장치도 함께 검사한다 — 운영 DB 에 붙을 수 있는 도구다.
/// </summary>
public static int RunQueryTrial(string reportPath)
{
var lines = new List<string>();
try
{
var workbench = new SheetMe.Data.Stores.OracleQueryWorkbench(ConfigService.Current.ConnectionString);
lines.Add($"DB 사용 가능: {workbench.CanUseDb}");
var cases = new (string Label, string Sql, bool ExpectOk)[]
{
("정상 조회", "SELECT ShtCod, ShtKorNam FROM E_ShtMst WHERE ROWNUM <= 3", true),
("없는 테이블", "SELECT * FROM 없는테이블_XYZ", false),
("없는 컬럼", "SELECT 없는컬럼_XYZ FROM E_ShtMst", false),
("구문 오류", "SELECT FROM WHERE", false),
("앞 주석 뒤 정상", "-- 설명\nSELECT 1 FROM DUAL", true),
("조회 아님(INSERT)", "INSERT INTO E_ShtMst (ShtCod) VALUES ('X')", false),
("조회 아님(DELETE)", "DELETE FROM E_ShtMst", false),
("조회 아님(DROP)", "DROP TABLE E_ShtMst", false),
("주석으로 가린 DELETE", "/* SELECT */ DELETE FROM E_ShtMst", false),
("빈 쿼리", " ", false),
};
var failures = 0;
foreach (var (label, sql, expectOk) in cases)
{
var result = workbench.Trial(sql);
var ok = result.Ok == expectOk;
if (!ok)
{
failures++;
}
lines.Add($"{(ok ? "PASS" : "FAIL")} {label,-18} → " +
(result.Ok ? result.Summary : "실패: " + result.Error));
}
var tables = workbench.ListTables();
var columns = workbench.ListColumns("E_ShtMst");
lines.Add(string.Empty);
lines.Add($"자동완성 원천 — 테이블·뷰 {tables.Count}건, E_ShtMst 컬럼 {columns.Count}건");
lines.Add(" 표본 테이블: " + string.Join(", ", tables.Take(8).Select(t => t.Name)));
lines.Add(" 표본 컬럼: " + string.Join(", ", columns.Take(10)));
lines.Add(string.Empty);
lines.Add($"결과: 실패 {failures}건");
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
return failures == 0 ? 0 : 1;
}
catch (Exception ex)
{
lines.Add("실패: " + ex);
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
return 1;
}
}
/// <summary>
/// 데이터·액션 태그 실사용 분포(--db-tags) — 태그 피커를 어떻게 정렬·큐레이션할지 근거(읽기 전용).
///