refs #734: add validated annotation PL/SQL API
This commit is contained in:
42
docs/design/734-sgmp-annotation-api/README.md
Normal file
42
docs/design/734-sgmp-annotation-api/README.md
Normal file
@@ -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 대상은 오류를 반환한다.
|
||||
148
sql/adb/76_sgmp_annotation_api.sql
Normal file
148
sql/adb/76_sgmp_annotation_api.sql
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user