refs #739: preserve Smilegate changes before repository layout migration
This commit is contained in:
@@ -83,25 +83,40 @@ public class SelectAiService {
|
||||
requireActiveToken(bearerToken);
|
||||
String normalizedPrompt = requiredPrompt(prompt);
|
||||
BackofficeProperties.SelectAi selectAi = requiredSelectAi();
|
||||
ResolvedExecutionScope scope = resolveExecutionScope(bearerToken, normalizedPrompt, scopeGameKey);
|
||||
|
||||
GameContext gameContext = resolveGameContext(bearerToken, normalizedPrompt);
|
||||
// Game identity is resolved only in ADB by sg_game_query_plan. That
|
||||
// function invokes OCI GenAI chat and validates its candidate choice
|
||||
// against the database catalog; Java never infers aliases or tables.
|
||||
QueryPlanContext queryPlan = requiredQueryPlan(
|
||||
gameCatalogVectorService.queryPlan(bearerToken, normalizedPrompt, 5));
|
||||
ResolvedExecutionScope scope = queryPlan.scope(scopeGameKey);
|
||||
return generateAndExecutePrepared(
|
||||
bearerToken,
|
||||
normalizedPrompt,
|
||||
selectAi,
|
||||
gameContext.prompt(scope.prompt()),
|
||||
gameContext.scopeType(),
|
||||
gameContext.status(),
|
||||
gameContext.candidates().size(),
|
||||
queryPlan.prompt(normalizedPrompt, scope.gameKey()),
|
||||
queryPlan.targetType(),
|
||||
queryPlan.status(),
|
||||
queryPlan.targetCount(),
|
||||
scope,
|
||||
null,
|
||||
null
|
||||
queryPlan.allowedPrefixes(),
|
||||
queryPlan,
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
|
||||
public JsonNode generateAndExecute(
|
||||
String bearerToken, String prompt, String scopeGameKey, JsonNode priorToolContext) {
|
||||
return generateAndExecute(bearerToken, prompt, scopeGameKey, priorToolContext, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses only rehydrated approved pattern ids from the preceding preflight.
|
||||
* The preflight runs before the OCI Chat game plan; the plan still controls
|
||||
* the final target-type compatibility and all game identity.
|
||||
*/
|
||||
public JsonNode generateAndExecute(
|
||||
String bearerToken, String prompt, String scopeGameKey, JsonNode priorToolContext,
|
||||
JsonNode fewShotPreflight) {
|
||||
if (priorToolContext == null || priorToolContext.isMissingNode()
|
||||
|| priorToolContext.isNull()) {
|
||||
return generateAndExecute(bearerToken, prompt, scopeGameKey);
|
||||
@@ -111,17 +126,20 @@ public class SelectAiService {
|
||||
BackofficeProperties.SelectAi selectAi = requiredSelectAi();
|
||||
QueryPlanContext queryPlan = requiredQueryPlan(priorToolContext);
|
||||
ResolvedExecutionScope scope = queryPlan.scope(scopeGameKey);
|
||||
List<QaVectorService.VectorExample> preflightExamples = approvedPreflightExamples(
|
||||
bearerToken, fewShotPreflight, queryPlan.targetType());
|
||||
return generateAndExecutePrepared(
|
||||
bearerToken,
|
||||
normalizedPrompt,
|
||||
selectAi,
|
||||
queryPlan.prompt(normalizedPrompt),
|
||||
queryPlan.prompt(normalizedPrompt, scope.gameKey()),
|
||||
queryPlan.targetType(),
|
||||
queryPlan.status(),
|
||||
queryPlan.targetCount(),
|
||||
scope,
|
||||
queryPlan.allowedPrefixes(),
|
||||
queryPlan
|
||||
queryPlan,
|
||||
preflightExamples
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,27 +153,14 @@ public class SelectAiService {
|
||||
int gameCandidateCount,
|
||||
ResolvedExecutionScope scope,
|
||||
Set<String> allowedPrefixes,
|
||||
QueryPlanContext queryPlan
|
||||
QueryPlanContext queryPlan,
|
||||
List<QaVectorService.VectorExample> preflightExamples
|
||||
) {
|
||||
EnrichedPrompt enrichedPrompt = enrichWithFewShot(
|
||||
bearerToken, selectAi, executionPrompt, queryPlan == null ? "ANY" : queryPlan.targetType());
|
||||
bearerToken, selectAi, originalPrompt, executionPrompt,
|
||||
queryPlan == null ? "ANY" : queryPlan.targetType(), preflightExamples);
|
||||
String generatedSql = generate(selectAi, enrichedPrompt.prompt(), "showsql");
|
||||
String normalizedSql = validateReadOnlySql(generatedSql);
|
||||
if (allowedPrefixes != null && gameScopeService != null
|
||||
&& gameScopeService.configured()
|
||||
&& gameScopeService.referencesGameScopedObjectOutsidePrefixes(
|
||||
bearerToken, normalizedSql, allowedPrefixes)) {
|
||||
// This is a model-output validation failure, not an answer fallback.
|
||||
// Give Select AI its own violated-plan feedback once and validate the
|
||||
// regenerated SQL under exactly the same read-only and scope rules.
|
||||
generatedSql = generate(selectAi, scopeCorrectionPrompt(enrichedPrompt.prompt()), "showsql");
|
||||
normalizedSql = validateReadOnlySql(generatedSql);
|
||||
if (gameScopeService.referencesGameScopedObjectOutsidePrefixes(
|
||||
bearerToken, normalizedSql, allowedPrefixes)) {
|
||||
throw new AppException(
|
||||
"Select AI 생성 SQL이 게임 질의 계획에 없는 prefix 전용 객체를 참조했습니다.");
|
||||
}
|
||||
}
|
||||
QueryExecution execution = executeReadOnly(selectAi, normalizedSql);
|
||||
ObjectNode response = objectMapper.createObjectNode();
|
||||
response.put("status", "SHOWSQL_AND_EXECUTED");
|
||||
@@ -168,6 +173,7 @@ public class SelectAiService {
|
||||
response.put("queryPlanTargetType", queryPlan.targetType());
|
||||
response.put("queryPlanStatus", queryPlan.status());
|
||||
response.put("queryPlanTargetCount", queryPlan.targetCount());
|
||||
response.put("selectAiReference", queryPlan.selectAiReference());
|
||||
}
|
||||
if (scope.gameKey() != null) {
|
||||
response.put("scopeGameKey", scope.gameKey());
|
||||
@@ -189,17 +195,37 @@ public class SelectAiService {
|
||||
return response;
|
||||
}
|
||||
|
||||
private List<QaVectorService.VectorExample> approvedPreflightExamples(
|
||||
String bearerToken, JsonNode fewShotPreflight, String targetType) {
|
||||
if (fewShotPreflight == null || fewShotPreflight.isNull() || fewShotPreflight.isMissingNode()
|
||||
|| qaVectorService == null) {
|
||||
return List.of();
|
||||
}
|
||||
JsonNode preflight = unwrapMcpToolResult(fewShotPreflight);
|
||||
if (!"FEWSHOT_PREFLIGHT".equals(preflight.path("status").asText())) {
|
||||
return List.of();
|
||||
}
|
||||
List<Long> ids = new java.util.ArrayList<>();
|
||||
for (JsonNode example : preflight.path("examples")) {
|
||||
if (example.path("exampleId").canConvertToLong()) {
|
||||
ids.add(example.path("exampleId").asLong());
|
||||
}
|
||||
}
|
||||
return qaVectorService.findApprovedExamples(bearerToken, ids, targetType);
|
||||
}
|
||||
|
||||
private QueryPlanContext requiredQueryPlan(JsonNode plan) {
|
||||
JsonNode normalizedPlan = unwrapMcpToolResult(plan);
|
||||
String targetType = normalizedPlan.path("targetType")
|
||||
.asText("").trim().toUpperCase(Locale.ROOT);
|
||||
if (!Set.of("NONE", "SINGLE", "MULTI", "ALL").contains(targetType)
|
||||
|| !normalizedPlan.path("targets").isArray()) {
|
||||
|| !normalizedPlan.path("gameTargets").isArray()
|
||||
|| normalizedPlan.path("selectAiReference").asText("").isBlank()) {
|
||||
throw new AppException(
|
||||
"queryPlan은 targetType(NONE/SINGLE/MULTI/ALL)과 targets 배열이 필요합니다.");
|
||||
"queryPlan은 targetType, gameTargets, selectAiReference가 필요합니다.");
|
||||
}
|
||||
Set<String> allowedPrefixes = new LinkedHashSet<>();
|
||||
for (JsonNode target : normalizedPlan.path("targets")) {
|
||||
for (JsonNode target : normalizedPlan.path("gameTargets")) {
|
||||
String prefix = target.path("gamePrefix").asText("").trim();
|
||||
if (!prefix.isEmpty()) {
|
||||
allowedPrefixes.add(prefix.toUpperCase(Locale.ROOT));
|
||||
@@ -208,8 +234,9 @@ public class SelectAiService {
|
||||
return new QueryPlanContext(
|
||||
targetType,
|
||||
normalizedPlan.path("status").asText(""),
|
||||
normalizedPlan.path("targets").size(),
|
||||
normalizedPlan.path("gameTargets").size(),
|
||||
Set.copyOf(allowedPrefixes),
|
||||
normalizedPlan.path("selectAiReference").asText().trim(),
|
||||
normalizedPlan.deepCopy()
|
||||
);
|
||||
}
|
||||
@@ -249,44 +276,6 @@ public class SelectAiService {
|
||||
return current == null ? objectMapper.createObjectNode() : current;
|
||||
}
|
||||
|
||||
private GameContext resolveGameContext(String bearerToken, String question) {
|
||||
try {
|
||||
List<GameCatalogVectorService.GameCandidate> candidates =
|
||||
gameCatalogVectorService.search(bearerToken, question, 5);
|
||||
String status = candidates.isEmpty() ? "NO_MATCH" : "RESOLVED";
|
||||
String scopeType = candidates.isEmpty() ? "UNKNOWN" : "SINGLE_GAME";
|
||||
return new GameContext(scopeType, status, candidates);
|
||||
} catch (Exception ignored) {
|
||||
return new GameContext("UNKNOWN", "UNAVAILABLE", List.of());
|
||||
}
|
||||
}
|
||||
|
||||
private ResolvedExecutionScope resolveExecutionScope(
|
||||
String bearerToken, String originalPrompt, String scopeGameKey
|
||||
) {
|
||||
String requestedKey = scopeGameKey == null ? "" : scopeGameKey.trim();
|
||||
if (requestedKey.isEmpty()) {
|
||||
return new ResolvedExecutionScope(originalPrompt, null, null);
|
||||
}
|
||||
if (gameScopeService == null) {
|
||||
throw new AppException("게임 범위 검증 서비스를 사용할 수 없습니다.");
|
||||
}
|
||||
GameScopeService.GameScope scope = gameScopeService.resolve(bearerToken, originalPrompt).scopes().stream()
|
||||
.filter(item -> requestedKey.equals(item.gameKey()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AppException("요청한 게임 범위가 DB 조회 결과에 없습니다."));
|
||||
if (!"SUPPORTED".equals(scope.status())) {
|
||||
throw new AppException("DB 게임 범위가 조회 실행을 허용하지 않습니다: " + scope.reasonCode());
|
||||
}
|
||||
String scopedPrompt = "Use only the DB-resolved game scope below. Do not generate SQL for any other "
|
||||
+ "game mentioned in the original question. The scope was validated through current game alias "
|
||||
+ "metadata and approved object availability.\n"
|
||||
+ "Resolved game key: " + scope.gameKey() + "\n"
|
||||
+ "Resolved display name: " + scope.displayName() + "\n"
|
||||
+ "Matched alias: " + scope.matchedAlias() + "\n\n"
|
||||
+ "Original user question:\n" + originalPrompt;
|
||||
return new ResolvedExecutionScope(scopedPrompt, scope.gameKey(), scope.displayName());
|
||||
}
|
||||
|
||||
private void addFewShotExamples(ObjectNode response, List<QaVectorService.VectorExample> examples) {
|
||||
ArrayNode items = response.putArray("fewShotExamples");
|
||||
@@ -319,7 +308,8 @@ public class SelectAiService {
|
||||
requireActiveToken(bearerToken);
|
||||
String normalizedPrompt = requiredPrompt(prompt);
|
||||
BackofficeProperties.SelectAi selectAi = requiredSelectAi();
|
||||
EnrichedPrompt enrichedPrompt = enrichWithFewShot(bearerToken, selectAi, normalizedPrompt, "ANY");
|
||||
EnrichedPrompt enrichedPrompt = enrichWithFewShot(
|
||||
bearerToken, selectAi, normalizedPrompt, normalizedPrompt, "ANY", List.of());
|
||||
String selectAiPrompt = generate(selectAi, enrichedPrompt.prompt(), "showprompt");
|
||||
|
||||
ObjectNode response = objectMapper.createObjectNode();
|
||||
@@ -391,27 +381,25 @@ public class SelectAiService {
|
||||
}
|
||||
|
||||
private EnrichedPrompt enrichWithFewShot(
|
||||
String bearerToken,
|
||||
BackofficeProperties.SelectAi selectAi,
|
||||
String prompt,
|
||||
String targetType
|
||||
) {
|
||||
String bearerToken, BackofficeProperties.SelectAi selectAi,
|
||||
String retrievalQuestion, String generationPrompt, String targetType,
|
||||
List<QaVectorService.VectorExample> preflightExamples) {
|
||||
if (!fewShotEnabled(selectAi) || qaVectorService == null) {
|
||||
return new EnrichedPrompt(prompt, "DISABLED", 0, List.of());
|
||||
return new EnrichedPrompt(generationPrompt, "DISABLED", 0, List.of());
|
||||
}
|
||||
try {
|
||||
List<QaVectorService.VectorExample> examples = qaVectorService
|
||||
.search(bearerToken, prompt, fewShotTopK(selectAi), targetType)
|
||||
.examples();
|
||||
List<QaVectorService.VectorExample> examples = preflightExamples == null || preflightExamples.isEmpty()
|
||||
? qaVectorService.search(bearerToken, retrievalQuestion, fewShotTopK(selectAi), targetType).examples()
|
||||
: preflightExamples;
|
||||
if (examples.isEmpty()) {
|
||||
return new EnrichedPrompt(composePolicyPrompt(prompt), "NO_MATCH", 0, List.of());
|
||||
return new EnrichedPrompt(composePolicyPrompt(generationPrompt), "NO_MATCH", 0, List.of());
|
||||
}
|
||||
return new EnrichedPrompt(
|
||||
composeFewShotPrompt(prompt, examples), "APPLIED", Math.min(examples.size(), MAX_FEW_SHOT_EXAMPLES),
|
||||
composeFewShotPrompt(generationPrompt, examples), "APPLIED", Math.min(examples.size(), MAX_FEW_SHOT_EXAMPLES),
|
||||
examples.subList(0, Math.min(examples.size(), MAX_FEW_SHOT_EXAMPLES)));
|
||||
} catch (Exception ignored) {
|
||||
// Vector retrieval is an optional prompt aid; preserve the normal Text2SQL path on failure.
|
||||
return new EnrichedPrompt(composePolicyPrompt(prompt), "UNAVAILABLE", 0, List.of());
|
||||
return new EnrichedPrompt(composePolicyPrompt(generationPrompt), "UNAVAILABLE", 0, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,28 +407,38 @@ public class SelectAiService {
|
||||
return POLICY_PREFIX
|
||||
+ "Resolve business terms and game names from the approved game-alias metadata before selecting a "
|
||||
+ "game-scoped object. A generic term such as common user means no particular game. If no game alias "
|
||||
+ "is resolved, do not substitute an arbitrary game-scoped object. Keep common-object questions "
|
||||
+ "game-neutral and preserve the resolver status for the answer layer.\n"
|
||||
+ "is resolved, do not substitute a game-specific object. Follow the authoritative scope guidance "
|
||||
+ "and preserve the resolver status for the answer layer.\n"
|
||||
+ "Original user question:\n" + prompt;
|
||||
}
|
||||
|
||||
static String scopeCorrectionPrompt(String originalPrompt) {
|
||||
return originalPrompt
|
||||
+ "\n\n[SQL VALIDATION FEEDBACK]\n"
|
||||
+ "The previous SQL selected a game-scoped object outside the authoritative query plan. "
|
||||
+ "Regenerate one read-only SQL statement using the same plan. Do not substitute any "
|
||||
+ "game-scoped object. Apply the active profile instructions and do not report a SQL failure.\n";
|
||||
}
|
||||
|
||||
static String composeFewShotPrompt(String prompt, List<QaVectorService.VectorExample> examples) {
|
||||
StringBuilder enriched = new StringBuilder(POLICY_PREFIX
|
||||
+ "The verified examples below are guidance only: use only relevant SQL patterns, do not invent "
|
||||
+ "Reference precedence:\n"
|
||||
+ "1. A [REQUIRED BOUNDARY REFERENCE] is a verified decision reference for its matching "
|
||||
+ "target type and logical object role. You must apply its boundary decision before generating SQL. "
|
||||
+ "Do not replace it with a game-scoped object.\n"
|
||||
+ "2. Normal SQL-pattern examples are required result-shape references when their logical object role "
|
||||
+ "and requested result grain match the original question. Preserve the matching aggregate versus "
|
||||
+ "individual-detail shape; do not replace an aggregate example with detail rows, or the reverse, "
|
||||
+ "unless the user explicitly asks for that different shape. Do not invent "
|
||||
+ "identifiers, and do not override current metadata or game-alias resolution policy. "
|
||||
+ "When examples use a game-specific object, reuse that pattern only after the current question "
|
||||
+ "When an example uses a game-specific object, reuse that pattern only after the current question "
|
||||
+ "resolves the same game alias; otherwise keep the query unscoped or request clarification.\n\n"
|
||||
+ "Verified few-shot examples:\n");
|
||||
int included = 0;
|
||||
+ "Verified few-shot references:\n");
|
||||
List<QaVectorService.VectorExample> ordered = new java.util.ArrayList<>(examples.size());
|
||||
for (QaVectorService.VectorExample example : examples) {
|
||||
if (isBoundaryReference(example)) {
|
||||
ordered.add(example);
|
||||
}
|
||||
}
|
||||
for (QaVectorService.VectorExample example : examples) {
|
||||
if (!isBoundaryReference(example)) {
|
||||
ordered.add(example);
|
||||
}
|
||||
}
|
||||
int included = 0;
|
||||
for (QaVectorService.VectorExample example : ordered) {
|
||||
if (included >= MAX_FEW_SHOT_EXAMPLES) {
|
||||
break;
|
||||
}
|
||||
@@ -467,21 +465,32 @@ public class SelectAiService {
|
||||
+ "\nExample " + index + " logical object role: "
|
||||
+ (example.objectRole() == null || example.objectRole().isBlank()
|
||||
? "UNSPECIFIED" : example.objectRole()) + "\n";
|
||||
if ("NO_TARGET".equals(example.referenceKind())
|
||||
|| "OBJECT_UNAVAILABLE".equals(example.referenceKind())) {
|
||||
if (isBoundaryReference(example)) {
|
||||
String boundary = truncate(example.answer(), MAX_FEW_SHOT_SQL_CHARS);
|
||||
String answerSql = truncate(example.answerSql(), MAX_FEW_SHOT_SQL_CHARS);
|
||||
if (boundary.isBlank() || answerSql.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
return prefix + "Boundary rule (when the current plan is NONE and this logical object role "
|
||||
+ "matches the requested operation, follow this boundary SQL template instead of "
|
||||
+ "substituting a game-scoped object; do not apply it to an approved common-object operation):\n"
|
||||
return "[REQUIRED BOUNDARY REFERENCE]\n" + prefix
|
||||
+ "This verified boundary reference must be applied when the current target type and logical "
|
||||
+ "object role match. Use this boundary SQL template instead of substituting a game-scoped object; "
|
||||
+ "do not apply it to an approved common-object operation:\n"
|
||||
+ boundary
|
||||
+ "\nVerified boundary SQL template:\n" + answerSql + "\n\n";
|
||||
}
|
||||
String answerSql = truncate(example.answerSql(), MAX_FEW_SHOT_SQL_CHARS);
|
||||
return answerSql.isBlank() ? "" : prefix + "Verified SQL template:\n" + answerSql + "\n\n";
|
||||
String answerGuide = truncate(example.answer(), MAX_FEW_SHOT_SQL_CHARS);
|
||||
if (answerSql.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
return prefix + "Verified SQL template:\n" + answerSql
|
||||
+ (answerGuide.isBlank() ? "" : "\nExpected answer guidance:\n" + answerGuide)
|
||||
+ "\n\n";
|
||||
}
|
||||
|
||||
private static boolean isBoundaryReference(QaVectorService.VectorExample example) {
|
||||
return "NO_TARGET".equals(example.referenceKind())
|
||||
|| "OBJECT_UNAVAILABLE".equals(example.referenceKind());
|
||||
}
|
||||
|
||||
private static String truncate(String value, int maxLength) {
|
||||
@@ -599,11 +608,21 @@ public class SelectAiService {
|
||||
String status,
|
||||
int targetCount,
|
||||
Set<String> allowedPrefixes,
|
||||
String selectAiReference,
|
||||
JsonNode plan
|
||||
) {
|
||||
String prompt(String originalQuestion) {
|
||||
return "[AUTHORITATIVE GAME QUERY PLAN]\n" + plan
|
||||
+ "\n[ORIGINAL USER QUESTION]\n" + originalQuestion;
|
||||
String prompt(String originalQuestion, String scopeGameKey) {
|
||||
if (scopeGameKey == null || scopeGameKey.isBlank()) {
|
||||
return selectAiReference + "\n\n[ORIGINAL USER QUESTION]\n" + originalQuestion;
|
||||
}
|
||||
if (plan.path("gameTargets").size() != 1) {
|
||||
throw new AppException("Worker queryPlan must contain exactly one target.");
|
||||
}
|
||||
JsonNode target = plan.path("gameTargets").get(0);
|
||||
if (!scopeGameKey.equals(target.path("gameKey").asText(""))) {
|
||||
throw new AppException("scopeGameKey가 worker queryPlan 대상과 일치하지 않습니다.");
|
||||
}
|
||||
return selectAiReference + "\n\n[TASK QUESTION]\n" + originalQuestion;
|
||||
}
|
||||
|
||||
ResolvedExecutionScope scope(String requestedGameKey) {
|
||||
@@ -611,7 +630,10 @@ public class SelectAiService {
|
||||
if (requested.isEmpty()) {
|
||||
return new ResolvedExecutionScope("", null, null);
|
||||
}
|
||||
for (JsonNode target : plan.path("targets")) {
|
||||
if (plan.path("gameTargets").size() != 1) {
|
||||
throw new AppException("Worker queryPlan must contain exactly one target.");
|
||||
}
|
||||
for (JsonNode target : plan.path("gameTargets")) {
|
||||
if (requested.equals(target.path("gameKey").asText(""))) {
|
||||
return new ResolvedExecutionScope(
|
||||
"", requested, target.path("gameName").asText(requested));
|
||||
@@ -621,29 +643,4 @@ public class SelectAiService {
|
||||
}
|
||||
}
|
||||
|
||||
private record GameContext(
|
||||
String scopeType, String status, List<GameCatalogVectorService.GameCandidate> candidates) {
|
||||
String prompt(String original) {
|
||||
StringBuilder context = new StringBuilder();
|
||||
context.append("[GAME CATALOG MATCH]\n")
|
||||
.append("scope_type: ").append(scopeType).append('\n')
|
||||
.append("status: ").append(status).append('\n');
|
||||
for (GameCatalogVectorService.GameCandidate candidate : candidates) {
|
||||
context.append("game_key: ").append(candidate.gameKey()).append('\n')
|
||||
.append("game_id: ").append(candidate.gameId()).append('\n')
|
||||
.append("game_prefix: ").append(candidate.gamePrefix()).append('\n')
|
||||
.append("game_name: ").append(candidate.gameName()).append('\n')
|
||||
.append("matched_aliases: ").append(candidate.aliases()).append('\n')
|
||||
.append("user_master_object_name: ")
|
||||
.append(candidate.userMasterObjectName()).append('\n')
|
||||
.append("cosine_distance: ").append(candidate.similarity()).append('\n');
|
||||
}
|
||||
context.append("[GAME SCOPE METADATA]\n")
|
||||
.append("The following resolver facts are authoritative metadata. ")
|
||||
.append("Apply the table and column annotations associated with these facts; ")
|
||||
.append("do not invent identifiers or scope rules.\n\n")
|
||||
.append(original);
|
||||
return context.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user