From d552e5e25b3a7520fd05f7757bde3b5505788512 Mon Sep 17 00:00:00 2001 From: devmrko Date: Fri, 24 Jul 2026 16:03:50 +0900 Subject: [PATCH] refs #734: add validated annotation PL/SQL API --- docs/design/734-sgmp-annotation-api/README.md | 42 +++++ sql/adb/76_sgmp_annotation_api.sql | 148 ++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 docs/design/734-sgmp-annotation-api/README.md create mode 100644 sql/adb/76_sgmp_annotation_api.sql diff --git a/docs/design/734-sgmp-annotation-api/README.md b/docs/design/734-sgmp-annotation-api/README.md new file mode 100644 index 0000000..046b76a --- /dev/null +++ b/docs/design/734-sgmp-annotation-api/README.md @@ -0,0 +1,42 @@ +# SGMP annotation 관리 API (#734) + +> 상태: Approved +> 구현: `sql/adb/76_sgmp_annotation_api.sql` +> 목적: 백오피스와 운영 스크립트가 동일한 검증 규칙으로 테이블/뷰 annotation을 추가·수정하도록 한다. + +## 입력 계약 + +`SGMP_SET_ANNOTATION`은 스키마, 대상 종류(`TABLE` 또는 `COLUMN`), 테이블/뷰 이름, 선택적 컬럼 이름, annotation 값, 선택적 annotation 이름을 받는다. annotation 이름 기본값은 `AI_GUIDANCE`이며, 이름을 명시하면 그 이름을 사용한다. + +컬럼 이름은 `COLUMN` 대상에서만 필수다. 값은 4,000자 이내이며 빈 값은 허용하지 않는다. 객체와 컬럼은 `ALL_OBJECTS`/`ALL_TAB_COLUMNS`에서 확인하고 식별자는 `DBMS_ASSERT`로 제한한다. + +## 동작 + +1. 대상 객체가 TABLE 또는 VIEW인지 확인한다. +2. VIEW의 COLUMN 대상은 Oracle에서 변경할 수 없으므로 명확한 오류로 거부한다. +3. `ALL_ANNOTATIONS_USAGE`에서 동일 annotation의 존재 여부를 확인한다. +4. 기존 값이 있으면 DROP 후 ADD, 없으면 ADD만 수행한다. +5. 수행 결과(`ADDED` 또는 `REPLACED`)와 정규화된 대상을 반환한다. + +DDL은 Oracle의 implicit commit 특성이 있으므로 호출자는 별도 트랜잭션으로 간주하지 않는다. 이 함수는 게임명·prefix·특정 테이블 정책을 하드코딩하지 않으며, 입력 객체의 존재와 Oracle 문법만 검증한다. + +## 호출 예 + +```sql +SELECT SGMP_SET_ANNOTATION( + 'SGMP_POC', 'TABLE', 'CZN_COMN_USER_MST', NULL, + 'AU는 최신 BASE_DT에서 AU_FLAG=1이고 EXPT_USER_YN=N인 활성 사용자 수다.', + 'AI_GUIDANCE' +) FROM dual; + +SELECT SGMP_SET_ANNOTATION( + 'SGMP_POC', 'COLUMN', 'CZN_COMN_USER_MST', 'AU_FLAG', + 'AU 집계용 활성 사용자 플래그(1=활성).', 'BUSINESS_DEFINITION' +) FROM dual; +``` + +## 테스트 기준 + +- TABLE 신규 annotation은 `ADDED`를 반환한다. +- 같은 이름을 다시 호출하면 기존 값을 교체하고 `REPLACED`를 반환한다. +- 존재하지 않는 객체/컬럼, 잘못된 대상 종류, VIEW의 COLUMN 대상은 오류를 반환한다. diff --git a/sql/adb/76_sgmp_annotation_api.sql b/sql/adb/76_sgmp_annotation_api.sql new file mode 100644 index 0000000..e30ae0e --- /dev/null +++ b/sql/adb/76_sgmp_annotation_api.sql @@ -0,0 +1,148 @@ +-- SGMP PoC: validated table/view and column annotation API +-- Issue: #734 + +CREATE OR REPLACE FUNCTION sgmp_set_annotation( + p_schema_name IN VARCHAR2, + p_target_kind IN VARCHAR2, + p_object_name IN VARCHAR2, + p_column_name IN VARCHAR2 DEFAULT NULL, + p_change_text IN VARCHAR2 DEFAULT NULL, + p_annotation_name IN VARCHAR2 DEFAULT 'AI_GUIDANCE' +) RETURN VARCHAR2 +AUTHID DEFINER +IS + l_schema_name VARCHAR2(128); + l_object_name VARCHAR2(128); + l_column_name VARCHAR2(128); + l_annotation_name VARCHAR2(128); + l_target_kind VARCHAR2(20); + l_object_type VARCHAR2(30); + l_exists PLS_INTEGER := 0; + l_action VARCHAR2(20); + l_sql VARCHAR2(32767); + + FUNCTION simple_name(p_value VARCHAR2, p_label VARCHAR2) RETURN VARCHAR2 IS + l_value VARCHAR2(128) := UPPER(TRIM(p_value)); + BEGIN + IF l_value IS NULL OR NOT REGEXP_LIKE(l_value, '^[A-Z][A-Z0-9_$#]{0,127}$') THEN + RAISE_APPLICATION_ERROR(-20001, p_label || ' has an invalid format.'); + END IF; + RETURN l_value; + END; + + FUNCTION qname(p_value VARCHAR2) RETURN VARCHAR2 IS + BEGIN + RETURN DBMS_ASSERT.ENQUOTE_NAME(p_value, FALSE); + END; + + FUNCTION literal(p_value VARCHAR2) RETURN VARCHAR2 IS + BEGIN + RETURN DBMS_ASSERT.ENQUOTE_LITERAL(p_value); + END; +BEGIN + l_schema_name := simple_name(p_schema_name, 'schema_name'); + l_object_name := simple_name(p_object_name, 'object_name'); + l_target_kind := UPPER(TRIM(p_target_kind)); + IF l_target_kind NOT IN ('TABLE', 'COLUMN') THEN + RAISE_APPLICATION_ERROR(-20002, 'target_kind must be TABLE or COLUMN.'); + END IF; + l_annotation_name := simple_name(p_annotation_name, 'annotation_name'); + IF p_change_text IS NULL OR LENGTH(p_change_text) = 0 THEN + RAISE_APPLICATION_ERROR(-20003, 'change_text must not be empty.'); + END IF; + IF LENGTH(p_change_text) > 4000 THEN + RAISE_APPLICATION_ERROR(-20004, 'change_text must be 4000 characters or less.'); + END IF; + + BEGIN + SELECT object_type + INTO l_object_type + FROM all_objects + WHERE owner = l_schema_name + AND object_name = l_object_name + AND object_type IN ('TABLE', 'VIEW') + AND ROWNUM = 1; + EXCEPTION + WHEN NO_DATA_FOUND THEN + RAISE_APPLICATION_ERROR(-20005, 'TABLE or VIEW object was not found.'); + END; + + IF l_target_kind = 'COLUMN' THEN + IF p_column_name IS NULL THEN + RAISE_APPLICATION_ERROR(-20006, 'column_name is required for COLUMN target.'); + END IF; + IF l_object_type = 'VIEW' THEN + RAISE_APPLICATION_ERROR(-20007, 'VIEW column annotations cannot be altered by Oracle.'); + END IF; + l_column_name := simple_name(p_column_name, 'column_name'); + BEGIN + SELECT 1 INTO l_exists + FROM all_tab_columns + WHERE owner = l_schema_name + AND table_name = l_object_name + AND column_name = l_column_name + AND ROWNUM = 1; + EXCEPTION + WHEN NO_DATA_FOUND THEN + RAISE_APPLICATION_ERROR(-20008, 'column_name does not exist on the table.'); + END; + ELSIF p_column_name IS NOT NULL THEN + RAISE_APPLICATION_ERROR(-20009, 'column_name is not allowed for TABLE target.'); + END IF; + + SELECT COUNT(*) + INTO l_exists + FROM all_annotations_usage + WHERE annotation_owner = l_schema_name + AND object_name = l_object_name + AND object_type = l_object_type + AND annotation_name = l_annotation_name + AND (l_target_kind = 'TABLE' AND column_name IS NULL + OR l_target_kind = 'COLUMN' AND column_name = l_column_name); + + IF l_exists > 0 THEN + IF l_target_kind = 'COLUMN' THEN + l_sql := 'ALTER TABLE ' || qname(l_schema_name) || '.' || qname(l_object_name) + || ' MODIFY ' || qname(l_column_name) || ' ANNOTATIONS (DROP ' + || qname(l_annotation_name) || ')'; + ELSIF l_object_type = 'VIEW' THEN + l_sql := 'ALTER VIEW ' || qname(l_schema_name) || '.' || qname(l_object_name) + || ' ANNOTATIONS (DROP ' || qname(l_annotation_name) || ')'; + ELSE + l_sql := 'ALTER TABLE ' || qname(l_schema_name) || '.' || qname(l_object_name) + || ' ANNOTATIONS (DROP ' || qname(l_annotation_name) || ')'; + END IF; + EXECUTE IMMEDIATE l_sql; + l_action := 'REPLACED'; + ELSE + l_action := 'ADDED'; + END IF; + + IF l_target_kind = 'COLUMN' THEN + l_sql := 'ALTER TABLE ' || qname(l_schema_name) || '.' || qname(l_object_name) + || ' MODIFY ' || qname(l_column_name) || ' ANNOTATIONS (ADD ' + || qname(l_annotation_name) || ' ' || literal(p_change_text) || ')'; + ELSIF l_object_type = 'VIEW' THEN + l_sql := 'ALTER VIEW ' || qname(l_schema_name) || '.' || qname(l_object_name) + || ' ANNOTATIONS (ADD ' || qname(l_annotation_name) || ' ' + || literal(p_change_text) || ')'; + ELSE + l_sql := 'ALTER TABLE ' || qname(l_schema_name) || '.' || qname(l_object_name) + || ' ANNOTATIONS (ADD ' || qname(l_annotation_name) || ' ' + || literal(p_change_text) || ')'; + END IF; + EXECUTE IMMEDIATE l_sql; + + RETURN l_action || ': ' || l_schema_name || '.' || l_object_name + || CASE WHEN l_column_name IS NULL THEN '' ELSE '.' || l_column_name END + || ' [' || l_annotation_name || ']'; +EXCEPTION + WHEN OTHERS THEN + IF SQLCODE BETWEEN -20099 AND -20000 THEN + RAISE; + END IF; + RAISE_APPLICATION_ERROR(-20099, 'annotation change failed: ' || SQLERRM); +END; +/ + +SHOW ERRORS FUNCTION sgmp_set_annotation;