★ API 계약 개선 포인트
이 문서가 답하는 질문
- 이 API 계약면에서 가장 위험한 것은 무엇인가?
- 어떤 함수가 서로 일관되지 않고, 왜 그것이 비용인가?
- 오류가 클라이언트에게 어떻게 잘못 전달되는가?
- 버전 호환과 폐기 경로는 어떻게 되어 있는가?
- 미구현 기능이 사용자에게 정직하게 보고되는가?
0. 요약 — 상위 5건
| 순위 | ID | 제목 | 왜 이것이 1순위인가 |
|---|---|---|---|
| 1 | API-01 | Cypher UNION이 조용히 결과를 잘라낸다 |
오류 없이 틀린 답을 준다. 계약 위반 중 유일하게 정답성을 해친다 |
| 2 | API-29 | Studio POST /api/sql이 인증 없이 임의 SQL을 실행 |
서버에 도달 가능한 누구든 DB 전권 |
| 3 | API-31 | SQLSTATE 규율 부재 — 모든 오류가 XX000 |
클라이언트가 재시도 가능 여부를 판단할 수 없다 |
| 4 | API-10 | og_explain_error가 라벨 오타를 못 잡는다 (UNKNOWN_LABEL 도달 불가) |
에이전트 자가 교정의 핵심 기능이 가장 흔한 실수에 무력 |
| 5 | API-05 | 세 함수가 SQL 조각을 그대로 보간 — 신뢰 경계 미문서화 | filter / policy_expr / source_table |
1. 개선 포인트 전체
| ID | 제목 | 심각도 | 근거 (파일:라인) | 현상 | 제안 | 예상 효과 | 리스크 |
|---|---|---|---|---|---|---|---|
| API-01 | Cypher UNION이 파싱만 되고 컴파일에서 조용히 버려짐 |
High | engine/src/cypher/parser.rs:161 (파싱), engine/src/cypher/ast.rs:214 (Query.union 필드), engine/src/cypher/compile.rs 전체에 union 참조 0건, compile.rs:351 compile_read가 q.clauses만 순회 |
MATCH (p:Person) RETURN p.name UNION MATCH (c:Company) RETURN c.name이 오류 없이 Person 결과만 반환. og_cypher_check()도 {"ok": true} |
compile_read 진입부에서 q.union.is_some()이면 즉시 Err("UNION is parsed but not compiled in this release — UNION the two og_cypher() calls in SQL instead"). 진짜 수정은 UNION/UNION ALL을 SQL UNION으로 컴파일하는 것 |
정답성 회복. 조용한 오답이 명시적 거부로 전환 | 이미 UNION 질의를 보내며 반쪽 결과를 정상으로 취급 중인 사용자가 있으면 즉시 깨진다 — 마이너 버전 경계에서 릴리스 노트와 함께 |
| API-02 | 인자 관례가 함수군마다 갈림 (그래프 식별자·방향 타입·보고서 이름) | Med | dir: og_reach는 "char" (engine/sql/access.sql:197), og_csr_build는 text (traverse.rs:298), og_degree는 text (adjacency.rs:77) · 그래프 식별: 대부분 graph text, og_relabel(graph_id int4) (labeling.rs:247), og_csr_*/og_check_integrity/og_add_prefix는 그래프 인자 없음 · 이름: og_interop_report(관계형)와 og_mapping_report(RDF 오버플로)가 서로 다른 것을 보고 |
호출자가 매번 캐스트를 기억해야 하고('o'::"char" vs 'o'), 그래프 스코프가 있는지 없는지 시그니처로 알 수 없다. 이름만으로 두 report 함수를 구별할 수 없다 |
(a) dir을 전부 text로 받는 래퍼를 추가하고 "char" 버전은 내부용으로 유지 (b) og_relabel(graph text) 오버로드 추가 (c) og_mapping_report → og_rdf_overflow_report 별칭 추가 후 구 이름 폐기 예고 |
학습 비용 감소, 오사용 감소 | SQL 함수 오버로드는 모호성 오류를 만들 수 있음 — 타입이 다르므로 실제로는 안전 |
| API-03 | 쓰기 DML의 반환 계약이 비일관 + 엣지 프로퍼티 수정 함수 부재 | Med | og_delete_node는 없는 id에도 1 반환 (storage/mod.rs:355, removed + 1), og_delete_edge는 0 반환 (storage/mod.rs:514) · og_set_node_props는 없는 id에 0행 UPDATE 후 성공 (storage/mod.rs:324) · set_edge_props_inner는 pub fn이지만 #[pg_extern]이 아님 (storage/mod.rs:328) |
반환값으로 "실제로 지웠는지" 알 수 없다. SQL에서 엣지 프로퍼티를 고치려면 Cypher SET을 거쳐야 한다 |
(a) og_delete_node가 노드 부재 시 0 반환하도록 통일 (b) og_set_node_props가 영향 행 수를 int8로 반환 (c) og_set_edge_props(id int8, props jsonb) 를 #[pg_extern]으로 공개 |
멱등성 판단 가능, DML 표면 완결 | (a)(b)는 반환 타입/의미 변경 — 메이저 경계 필요 |
| API-04 | 프로퍼티 → 물리 컬럼 매핑이 대소문자를 지워 충돌 | Med | engine/src/catalog/types.rs:53 column_name()이 to_ascii_lowercase() 적용. og_catalog.property의 유일 제약은 (type_id, name) (engine/sql/bootstrap.sql:99)이므로 userName과 username은 둘 다 선언 가능 |
두 프로퍼티가 같은 컬럼 p_username을 공유. ALTER TABLE ... ADD COLUMN IF NOT EXISTS가 두 번째를 조용히 건너뛰어, 타입이 달라도 첫 번째 타입으로 저장된다. Cypher는 프로퍼티 이름을 대소문자 구분한다(compile.rs:1584 quote_ident) |
og_add_property에서 column_name(prop)의 충돌을 사전 검사하고 거부: property '<new>' maps to the same column as '<existing>' (case-insensitive); rename one. 또는 컬럼 이름에 짧은 해시 접미사 부여 |
조용한 데이터 손상 제거 | 기존 그래프에 이미 충돌이 있으면 마이그레이션 필요 |
| API-05 | 세 함수가 SQL 조각을 그대로 보간하나 신뢰 경계가 문서화되지 않음 | High | og_vector_search(filter) → format!("AND ({f})") (vector/mod.rs:116) · og_enable_rls(policy_expr) → CREATE POLICY ... USING ({policy_expr}) (interop/mod.rs:27) · og_map_table(source_table, id_column) → FROM {source_table}, ({id_column})::int8 (interop/mod.rs:75, :99) |
og_cypher는 "주입이 구조적으로 불가능"을 보장하는데(spec 003 FR-026), 같은 SQL 표면의 이 세 함수는 정반대다. docs/api.md:131은 filter를 "a SQL predicate"라고만 적고 신뢰 요구를 말하지 않는다 |
(a) 세 함수의 doc comment와 COMMENT ON FUNCTION에 신뢰 경계를 명시 (b) filter는 jsonb 술어 DSL({"p_year": {"gt": 2000}}) 대안을 추가하고 원시 문자열은 별도 _unsafe 접미사 함수로 분리 (c) 최소한 og_vector_search에서 세미콜론·주석 토큰 거부 |
오사용으로 인한 권한 상승 차단 | (b)는 새 API 표면 — 기존 호출자 유지 필요 |
| API-06 | og_create_index가 프로퍼티 선언 여부를 확인하지 않음 |
Low | engine/src/catalog/types.rs:604 — column_name(prop)만 계산하고 존재 확인 없이 CREATE INDEX. 실패 시 .expect("index creation failed") (types.rs:611). Cypher DDL 경로는 ensure_property로 막는다 (compat/ddl.rs:132) |
SQL 함수 직접 호출 시 ERROR: index creation failed — 원인(컬럼 없음)이 메시지에 없다 |
og_catalog.property에서 존재 확인 후 미선언이면 property '<p>' is not declared on '<T>' — call og_add_property first, or use the Cypher CREATE INDEX form which declares it |
오류 메시지가 자기 교정 가능해짐 | 없음 |
| API-07 | 참조 무결성이 강제되지 않음 (og_create_edge의 끝점, og_drop_graph의 잔여 행) |
Med | og_create_edge는 src/dst 노드 존재를 확인하지 않음 (storage/mod.rs:398~) — 역할이 선언되지 않은 관계 타입이면 검사 자체가 없다 (storage/mod.rs:455 validate_roles는 player_type_id IS NOT NULL인 역할만) · og_drop_graph는 저장 테이블과 graph 행만 지우고 og_node/og_edge/og_adj 잔여를 남긴다 (types.rs:334) · 이 두 결함을 og_check_integrity()가 사후에 잡는다 (stats.rs:244 orphan_node) |
유령 엣지와 고아 노드가 만들어지고, 발견은 별도 함수를 수동 실행할 때뿐 | (a) og_create_edge에서 og_data.og_node에 대한 존재 확인 추가 (EXISTS 한 번) (b) og_drop_graph가 og_drop_type과 같은 정리를 수행 (c) og_data.og_edge(src)/(dst) 에 FK 추가 검토 |
데이터 무결성 사전 보장 | (a)는 엣지 생성마다 조회 1회 추가 — 벌크 로드 성능 영향. (c)는 대량 삭제 비용 증가 |
| API-08 | 검증 함수 쌍의 오류 규약이 서로 다름 | Low | og_cypher_check(query)는 파싱 실패를 {"ok": false, "error": …} 로 반환 (cypher/mod.rs:699) · og_cypher_columns(query)는 같은 실패에 error!(e) 로 던진다 (cypher/mod.rs:722) · Bolt는 둘을 연속 호출하므로 첫 번째가 통과한 질의만 두 번째에 도달해 문제가 드러나지 않는다 (bolt/src/session.rs:283) |
두 "파싱 전용" 함수가 같은 입력에 다르게 반응. 클라이언트가 한쪽만 보고 규약을 일반화하면 깨진다 | og_cypher_columns도 실패 시 빈 배열을 반환하거나(현재 RETURN * 처리와 동일), 두 함수 모두 던지도록 통일 |
예측 가능한 검증 표면 | 반환 의미 변경 — 마이너 경계 |
| API-09 | 오류 메시지의 "supported:" 목록이 실제 지원 목록과 불일치 | Low | 함수: cypher/compile.rs:1559 목록에 실제 지원하는 elementId, element_at, stdev, nodes, relationships, vector.l2, similarity, upper, lower 누락 · 프로시저: compat/procs.rs:154 목록에 apoc.neighbors.tohop.count와 NO_OPS 5종(compat/procs.rs:72) 누락 |
지원하는 기능을 "지원하지 않는다"고 읽게 만든다. LLM이 이 목록을 근거로 대안을 고르면 잘못된 방향으로 간다 | 목록을 상수로 뽑아 match 팔과 한 곳에서 관리하고, 오류 메시지가 그 상수를 렌더링하도록 변경 |
자기 교정 정확도 상승, 문서-코드 동기화 | 없음 |
| API-10 | og_explain_error의 두 코드가 도달 불가, 문서 예제도 틀림 |
High | agent/mod.rs:294 classify()가 "unknown label"을 검사하나 grep -rn 'unknown label' engine/src/ 결과는 classify() 자신, suggestions() 자신, types.rs:203의 주석뿐 — 실제 라벨 부재는 pgrx::notice! (types.rs:168) · "not supported" 를 포함하는 Cypher 컴파일 오류도 없음 · docs/cypher.md:249은 unknown function 입력에 UNSUPPORTED_SYNTAX가 나온다고 적으나 classify() 순서상 UNKNOWN_FUNCTION · docs/cypher.md:295은 존재하지 않는 ERROR: unknown label … 텍스트를 인용 |
라벨 오타 — LLM이 가장 많이 내는 실수 — 에 og_explain_error가 {"ok": true}를 반환한다. 목적한 기능이 목적한 사례에 무력 |
(a) 컴파일러가 LabelMatch::Nothing을 만나면 그 사실을 Compiler.notes에 기록하고, og_explain_error가 notes를 읽어 {"ok": false, "code": "UNKNOWN_LABEL", …} 를 반환 (실행 의미론은 그대로 유지 — 진단 함수만 더 엄격하게) (b) classify()를 문자열 매칭에서 구조화된 오류 enum으로 교체 (c) docs/cypher.md의 두 예제 수정 |
에이전트 1회 재시도 교정률 상승 | (a)는 og_explain_error의 ok 의미가 "실행 가능"에서 "의도대로 동작"으로 좁아짐 — 문서화 필요 |
| API-11 | og_typeql(_params)가 시그니처에만 존재하고 무시됨 |
Med | engine/src/typeql/mod.rs:52 — 인자명이 _params이고 본문이 사용하지 않음. run_script(gid, query)만 호출 (typeql/mod.rs:56) · docs/api.md:113은 og_typeql(graph, query, params = '{}')로 표기 |
TypeQL 사용자는 값을 질의 텍스트에 넣을 수밖에 없다 → Cypher가 구조적으로 막은 주입 경로가 TypeQL에는 열려 있다. 시그니처가 안전한 방법이 있는 것처럼 보이게 한다 | 단기: 인자를 제거하거나, 비어 있지 않은 값이 오면 Err("TypeQL parameters are not implemented; inline the value or use og_cypher") · 장기: TypeQL 리터럴 위치에 $name 바인딩 지원 |
잘못된 안전 기대 제거, 주입 위험 감소 | 인자 제거는 시그니처 변경 — 기존 호출 코드가 깨짐. 오류를 던지는 쪽이 안전 |
| API-12 | TypeQL 스키마 UDF가 파싱만 되고 호출 시 무관한 오류 | Low | engine/src/typeql/parser.rs:919 — fun 본문을 return까지 스킵한다. 평가 경로 없음 · 선언한 함수를 질의에서 부르면 표현식 파서로 넘어가 <pos>: unexpected name '<name>' in an expression (typeql/parser.rs:713) · README 스펙 표: "spec 010 partial (UDF는 파싱/왕복만)" |
define은 성공하고 사용은 "알 수 없는 이름"으로 실패 — 사용자가 오타로 오인한다 |
define 시 함수 이름을 카탈로그에 기록하고, 표현식 파서가 그 이름을 만나면 function '<name>' is declared but query-local function evaluation is not implemented yet (spec 010 phase 6) 로 거부 |
미구현이 미구현으로 보임 | 없음 |
| API-13 | 백엔드 로컬 CSR이 커넥션 풀과 근본적으로 맞지 않음 | Med | engine/src/storage/traverse.rs:205 — thread_local! 에 백엔드당 1개. og_csr_reach/og_csr_hops가 없으면 error!("no compiled graph in this backend — call og_csr_build() first") (traverse.rs:340) · og_csr_build는 VOLATILE (traverse.rs:295) |
pgBouncer / 애플리케이션 풀 뒤에서는 어느 백엔드가 CSR을 들고 있는지 보장 불가. 간헐적 실패로 나타난다 | (a) og_csr_reach/og_csr_hops에 auto_build bool DEFAULT false 인자를 추가해 없으면 자동 빌드 (b) 함수 doc comment와 docs/api.md에 "세션 고정(session pinning)이 필요하다"를 명시 (c) og_csr_stats()가 빌드 인자(etypes, dir)를 그대로 반환해 재빌드가 가능하게 (현재 built_for는 "o:[3, 7]" 같은 사람용 문자열, traverse.rs:212) |
운영 사고 예방 | (a)는 예기치 않은 긴 지연을 만들 수 있음 — 기본 false 유지 필수 |
| API-14 | 통계·무결성 함수의 스코프가 인자와 불일치 | Low | og_graph_stats(graph)의 adjacency.* 블록이 og_data.og_adj 전체를 집계 — graph 필터 없음 (storage/stats.rs:46) · og_check_integrity()는 인자가 없어 그래프별 검사 불가 (stats.rs:172) · 각 검사가 LIMIT 100 (stats.rs:188, :211, :230, :248) |
다중 그래프 인스턴스에서 og_graph_stats('a')와 og_graph_stats('b')의 adjacency 블록이 동일. 무결성 위반 개수를 알 수 없다(최대 400) |
(a) adjacency 집계에 graph_id 조인 추가 (og_degree_distribution은 이미 한다, stats.rs:95) (b) og_check_integrity(graph text DEFAULT NULL, limit int4 DEFAULT 100) 오버로드 (c) 결과에 truncated 표시 추가 |
다중 테넌트 운영에서 통계가 의미를 가짐 | (a)는 og_adj에 그래프 조인이 들어가 통계 질의가 느려짐 |
| API-15 | 벡터 점수의 의미가 함수·메트릭마다 다른데 문서는 단일 규칙을 주장 | Med | og_vector_search: cosine 1 - d, ip -d, l2는 원 거리 d (vector/mod.rs:120) · og_vector_search_exact: 정규화 없이 원 거리 + entity 컬럼 없음 (vector/mod.rs:430, :418) · og_hybrid_search: l2에 1/(1+d) 라는 또 다른 변환 (vector/mod.rs:260) · docs/api.md:139: "Scores are normalised so higher is always better" |
재현율 측정(exact vs search 비교)이 두 함수의 점수를 직접 비교할 수 없고, l2 사용자는 정렬 방향을 반대로 이해한다. 세 번째 변환식은 문서에 없다 |
(a) 세 함수의 점수 변환을 하나의 헬퍼로 통합 (b) og_vector_search_exact에 entity jsonb 컬럼 추가로 반환 형태 통일 (c) 반환 컬럼을 score와 distance 두 개로 분리해 모호성 제거 (d) docs/api.md의 문장 수정 |
점수 해석 오류 제거, 재현율 벤치 신뢰성 | (b)(c)는 TABLE(...) 시그니처 변경 — 메이저 경계 |
| API-16 | og_add_embedding의 dims 상한(16000)이 실제 인덱스 한계(2000)와 불일치 |
Low | vector/mod.rs:41 — if !(1..=16000).contains(&dims) · 그러나 compat/genai.rs:157 주석: "pgvector's HNSW index stops at 2000 dimensions" · dims > 2000이면 프로퍼티 선언은 성공하고 CREATE INDEX ... USING hnsw에서 failed to build HNSW index: <e> (vector/mod.rs:62) |
부분 성공 상태: 컬럼은 생겼고 카탈로그 등록은 안 됐으며 인덱스는 없다. 사용자는 어느 단계까지 진행됐는지 모른다 | (a) dims > 2000이면 선언 전에 거부하고 이유를 제시: pgvector's HNSW index supports at most 2000 dimensions; truncate with genai.dimensions or declare the property without an index (b) 또는 build_index bool DEFAULT true 인자를 추가해 인덱스 없는 선언을 명시적으로 허용 |
부분 실패 상태 제거 | (b)는 새 인자 — 기본값이 현행과 같으므로 안전 |
| API-17 | 질의 벡터 차원 검사가 쉼표 세기 | Low | vector/mod.rs:134 — query.matches(',').count() + 1 != dims as usize |
'[1,2,3,]', '[1, 2, 3]', 잘못된 형식 모두에 대해 판정이 무의미하거나 오도적. og_vector_search_exact에는 이 검사조차 없다 |
pgvector가 캐스트하도록 두고($1::vector), 실패 시 오류 메시지를 감싸는 방식으로 전환: query vector could not be parsed as vector(<dims>): <pg error>. 또는 float8[] 를 받는 오버로드 추가 |
오류 메시지 정확도 상승 | 없음 |
| API-18 | og_similar(graph) 인자가 무시됨 |
Low | vector/mod.rs:173 — let _ = graph;. 타입은 id 비트에서 얻는다 (vector/mod.rs:165) |
다른 그래프의 id를 넘겨도 조용히 동작. 시그니처가 그래프 스코프를 약속하지만 지키지 않는다 | graph로 graph_id를 구하고 id_type(id)가 그 그래프에 속하는지 검증 후 불일치 시 거부. 또는 인자를 제거 |
스코프 약속과 동작 일치 | 인자 제거는 시그니처 변경 — 검증 추가 쪽이 안전 |
| API-19 | og_hybrid_search가 관계 타입에 동작하지 않음 (형제 함수와 비대칭) |
Med | og_vector_search(vector/mod.rs:111)와 og_similar(vector/mod.rs:169)는 types::type_kind(tid) == 'r' 로 엣지를 분기 · og_hybrid_search는 ensure_view(tid, false) 고정 (vector/mod.rs:247), 결과도 og_node_json(id) 고정 (vector/mod.rs:276) |
spec 004 FR-002("관계 임베딩 1급")가 하이브리드 검색에서만 깨진다. 관계 타입을 넘기면 잘못된 뷰를 조회해 빈 결과 또는 오류 | is_edge 분기를 추가하거나, 최소한 관계 타입 입력을 명시적으로 거부: og_hybrid_search does not support relationship types yet; use og_vector_search |
프로젝트의 핵심 차별점이 전 표면에서 일관 | 없음 (거부만 추가하는 경우) |
| API-20 | og_mark_embedded가 조용히 무연산이 되는 두 경로 |
Low | vector/mod.rs:368 — source_prop이 없으면 return, 저장 테이블이 없으면 return |
호출자가 "기록됨"과 "무시됨"을 구별할 수 없다. og_stale_embeddings가 계속 같은 행을 반환하는 이유를 알 수 없다 |
반환 타입을 bool 로 바꾸거나, 무시 사유를 NOTICE로 알림: no source property is declared for '<prop>'; staleness is not tracked |
무한 재임베딩 루프 방지 | 반환 타입 변경은 시그니처 변경 — NOTICE 쪽이 안전 |
| API-21 | og_apply_role의 max_rows가 무연산 · limits 스키마 미검증 · 결과가 거짓 보고 |
Med | agent/mod.rs:437 — SET og.max_rows = <n> 을 실행하지만 grep -rn 'max_rows' engine/ portal/ bolt/ 결과는 이 쓰기 두 줄뿐 — 읽는 코드 없음 · 네 SET 모두 .ok() 로 결과 폐기 (agent/mod.rs:427~:439) · 반환 applied가 저장된 limits 원본 그대로 (agent/mod.rs:440) · og_create_role은 limits jsonb를 검증하지 않음 (agent/mod.rs:405) · docs/api.md:178은 "row caps"를 기능으로 표기 |
행 제한을 설정했다고 믿는 운영자가 실제로는 아무 보호를 받지 못한다. 오타 난 키(statement_timeout)도 조용히 무시된다 |
(a) max_rows를 실제로 강제하거나(예: og_cypher가 current_setting('og.max_rows', true)를 읽어 결과를 자름) 인식 키에서 제거 (b) limits의 알 수 없는 키를 NOTICE로 보고 (c) 반환 applied를 실제로 성공한 SET만 담도록 변경 (d) docs/api.md 수정 |
가드레일이 실제 가드레일이 됨 | (a)에서 결과를 자르면 조용한 절단이 새 문제 — 자를 때 NOTICE 필수 |
| API-22 | 켜는 함수만 있고 끄는 함수가 없는 기능들 | Low | og_enable_history (agent/mod.rs:448) ↔︎ og_disable_history 없음 · og_enable_rls (interop/mod.rs:18) ↔︎ og_disable_rls 없음 · og_map_table (interop/mod.rs:60) ↔︎ og_unmap_table 없음 · og_add_embedding (vector/mod.rs:32) ↔︎ og_drop_embedding 없음 |
되돌리려면 DROP TRIGGER / ALTER TABLE ... DISABLE ROW LEVEL SECURITY / DROP VIEW 를 내부 테이블 이름(n_<tid>)을 알아내서 직접 실행해야 한다 — 내부 구현에 의존하는 운영 절차 |
대칭 함수 4개 추가. 최소한 각 enable_* 함수의 doc comment에 되돌리는 정확한 SQL을 기재 |
운영 절차가 공개 API 안에 머묾 | 없음 |
| API-23 | og_map_table이 경고 없이 DROP TABLE ... CASCADE, og_materialize_mapping이 실패를 삼킴 |
High | interop/mod.rs:97 — Spi::run(&format!("DROP TABLE IF EXISTS {table} CASCADE")).ok() 를 뷰 생성 직전에 무조건 실행 · og_materialize_mapping의 PK 추가(interop/mod.rs:144), 레지스트리 등록(:155, :160), 매핑 플래그(:165)가 전부 .ok() |
데이터가 들어 있는 네이티브 타입에 og_map_table을 부르면 그 데이터가 조용히 삭제된다. og_materialize_mapping은 PK 없이 성공을 반환할 수 있다 |
(a) og_map_table이 대상 테이블에 행이 있으면 거부하고 force bool DEFAULT false 를 요구 (b) og_materialize_mapping의 .ok() 를 `.unwrap_or_else( |
e | error!(...))` 로 전환 (c) 두 함수를 명시적 트랜잭션 경계로 감쌈 |
| API-24 | 포맷 인자 검증이 함수 쌍에서 비대칭 | Low | og_load_rdf는 ["turtle","ttl","ntriples","nt","n3"] 를 검사하고 거부 (adapters/rdf.rs:382) · og_dump_rdf는 matches!(..., "ntriples" | "nt") 만 보고 나머지 전부를 Turtle로 처리 (adapters/rdf.rs:697) |
og_dump_rdf(g, 'ntriple') (오타)가 조용히 Turtle을 반환. 왕복 테스트가 잘못된 포맷으로 통과 |
og_dump_rdf도 같은 허용 목록으로 검증하고 unsupported RDF format '<f>' (turtle | ntriples) 로 거부 |
왕복 검증 신뢰성 | 오타를 넣고 있던 호출자가 실패하게 됨 — 그게 옳다 |
| API-25 | 미구현 언어(SPARQL, SHACL)가 "함수 없음" 오류로 나타남 | Low | grep -rn -i 'sparql|shacl' engine/src/ → engine/src/lib.rs:15 모듈 표 주석 한 줄뿐. 파서·진입 함수·스텁 전무 · README 스펙 표는 spec 006을 "partial — … SPARQL not yet" 로 표기 (README.md:148) |
사용자는 ERROR: function og_sparql(unknown, unknown) does not exist 를 본다 — 오타인지 미구현인지 구별 불가. Cypher UNION, TypeQL undefine은 이유를 말해 주는데 여기만 침묵 |
스텁 함수 추가: og_sparql(graph text, query text) 가 error!("SPARQL is not implemented in this release (spec 006 is partial). Use og_cypher() over the RDF-mapped graph, or og_dump_rdf() and query externally.") · SHACL도 동일 |
미구현이 제품 결정으로 보임 | 존재하지 않는 기능의 표면이 생김 — 이름 선점 효과는 오히려 이득 |
| API-26 | DROP INDEX 이후 물리 인덱스가 남음 |
Low | compat/ddl.rs:336 — 카탈로그 항목만 DELETE FROM og_catalog.compat_index. 주석: "The catalog entry goes; the underlying column and its index stay" |
인덱스를 지워도 쓰기 비용이 그대로. pg_indexes를 봐야 실제 상태를 안다 |
(a) B-tree/fulltext/vector 인덱스는 실제로 DROP INDEX 하고 컬럼과 데이터는 유지 (현재 주석의 우려는 컬럼 드롭에 대한 것이므로 인덱스 드롭은 안전) (b) 최소한 반환 요약이나 NOTICE로 "physical index <name> was kept" 를 알림 |
DROP INDEX가 Neo4j 의미론과 일치 |
(a)는 같은 컬럼에 다른 이름의 인덱스가 있을 때 잘못 지울 수 있음 — 이름 기반 추적 필요 |
| API-27 | Neo4j 호환 표면의 작은 불충실성 두 건 | Low | compat/procs.rs:78 — NO_OPS에 "db.resamplindex" (Neo4j 실제 이름은 db.resampleIndex). 소문자 비교이므로 정확한 이름은 매치되지 않아 procedure '…' is not available 로 거부됨 · compat/procs.rs:251 주석은 "A filter naming types is accepted and its type names honoured" 라고 하나, procs.rs:264는 og_vlp(..., NULL::int4[], ...) 를 넘겨 타입 필터를 무시 |
드라이버 시작 시퀀스가 db.resampleIndex에서 실패할 수 있다. apoc.neighbors.tohop(n, 'KNOWS>', 2)가 모든 관계 타입을 따라간다 — 틀린 답 |
(a) NO_OPS에 "db.resampleindex" 추가(오타 항목은 호환을 위해 유지 가능) (b) relFilter의 타입 이름을 파싱해 og_type_id로 해석한 뒤 etypes 배열로 전달, 또는 주석을 실제 동작에 맞게 수정하고 타입 이름이 있으면 거부 |
호환면 정확도 | (b)의 APOC relFilter 문법은 <TYPE, TYPE>, TYPE, > 등 조합이 많음 — 부분 지원 시 경계 명시 필요 |
| API-28 | Bolt 게이트웨이 계약의 세 가지 미명시 한계 | Med | 시간/공간 구조체가 "<unsupported struct 0x<sig>>" 문자열로 대체됨 (bolt/src/session.rs:559) — 주석은 "silently mangling them would be worse" 라고 하나 이것이 곧 mangling · bookmark가 항상 상수 "ontological:0", t_first/t_last가 항상 0 (session.rs:326, session.rs:359, :378) · RUN 시점에 전체 결과를 메모리로 적재 (session.rs:294), PULL n은 이미 가져온 것을 나눌 뿐 · type '<t>' does not exist 가 Neo.ClientError.Database.DatabaseNotFound 로 분류됨 (session.rs:578) |
드라이버의 인과적 일관성(북마크) 기능이 무의미하게 동작. 큰 결과 집합에서 게이트웨이 메모리가 결과 크기에 비례. 타입 오타가 "데이터베이스 없음"으로 보고됨 | (a) 시간/공간 구조체는 문자열 대체 대신 Neo.ClientError.Statement.TypeError 로 거부 (b) bookmark를 pg_current_wal_lsn() 기반으로 생성하거나, 지원하지 않음을 명시(빈 문자열) (c) PULL n 을 커서 기반으로 전환 (d) 코드 매핑을 메시지 매칭에서 SQLSTATE 기반으로 전환 (API-31과 연동) |
드라이버 기대와 실제 동작 일치 | (c)는 게이트웨이에 상태(커서)가 생김 — "상태 없음" 설계 원칙과 충돌. 트레이드오프 명시 필요 |
| API-29 | Studio HTTP API에 인증이 전혀 없고 임의 SQL 라우트가 열려 있음 | High | portal/server/index.js:296 — POST /api/sql 이 pool.query(sql) 로 본문을 그대로 실행 · index.js:140의 라우트 객체 어디에도 인증 검사 없음 · POST /api/cypher(:182)는 쓰기 Cypher를 그대로 실행 · GET /api/audit(:283)은 질의 전문을 노출 · CORS 정책·레이트 리밋·보안 헤더 없음 |
서버에 도달 가능한 누구든 PGUSER 권한 전체를 행사한다. 감사 로그로 과거 질의(및 그 안에 인라인된 값)를 읽을 수 있다 |
(a) POST /api/sql 을 OG_STUDIO_ALLOW_RAW_SQL=1 환경변수로 게이팅하고 기본 비활성 (b) 최소한의 토큰 인증(OG_STUDIO_TOKEN + Authorization 헤더) 추가 (c) README/기동 배너에 "로컬 개발 전용" 명시 (d) 기본 바인딩을 127.0.0.1로 (현재 node:http 기본은 모든 인터페이스) |
사고 표면 축소 | (a)는 현재 Studio UI의 SQL 패널이 깨질 수 있음 — 환경변수로 켜면 유지 |
| API-30 | Studio HTTP 응답 계약의 부정확성 | Med | index.js:205 — columns: rows.length ? Object.keys(rows[0]) : []. jsonb는 키를 정렬하므로 RETURN 순서가 아니다. 엔진에 정답(og_cypher_columns)이 있는데 쓰지 않는다 · 모든 DB 오류가 400 (index.js:177, :211, :228, :251, :278, :291, :306) — 서버 오류와 클라이언트 오류가 구별되지 않음 · readBody가 Content-Type 미검사, 4MB 초과 시 reject가 500 이 됨 (index.js:53) · 메서드 불일치가 405가 아니라 404 text/plain (index.js:348) |
프런트엔드가 컬럼 순서를 신뢰할 수 없고, 클라이언트가 재시도 여부를 판단할 수 없다 | (a) /api/cypher가 og_cypher_columns(query) 를 호출하고 빈 배열이면 첫 행 키로 폴백 (Bolt가 하는 방식, bolt/src/session.rs:283) (b) pgError의 SQLSTATE 클래스로 4xx/5xx 분기 (API-31 이후) (c) 알려진 경로에 메서드가 다르면 405 반환 (d) 본문 초과를 413으로 |
프런트/클라이언트 계약 정확도 | (a)는 요청당 DB 왕복 1회 추가 — /api/cypher는 이미 og_cypher_sql을 부르므로 미미 |
| API-31 | SQLSTATE 규율 부재 — 모든 오류가 XX000, 내부 메시지·컴파일 SQL 노출, 감사 누락 |
High | grep -rn 'ERRCODE|PgSqlErrorCode|ereport|sqlstate' engine/src/ → 실제 사용 0건 (id.rs:31의 주석만 매치) · error! 115회, .expect("…") 111회, .unwrap() 75회 · .expect("property insert failed") (catalog/types.rs:545) 같은 내부 문구가 그대로 사용자 오류가 됨 · cypher execution failed: <e>\n--- compiled SQL ---\n<sql> (cypher/mod.rs:149) 가 내부 스키마 전체를 노출 · audit() 는 파싱 실패만 기록하고 컴파일/실행 실패는 error! 로 즉시 중단되어 기록되지 않음 (cypher/mod.rs:140, :149) |
클라이언트가 오류를 분류할 수 없어 세 계층(agent::classify, Bolt::from_pg, Studio)이 각자 문자열 매칭을 한다 — 셋 다 §API-10의 도달 불가 분기를 포함 · 실패한 질의가 감사 로그에 남지 않아 사후 분석 불가 |
(a) 오류 종류별 SQLSTATE 부여: 라벨/타입 미존재 → 42704 undefined_object, 함수 미존재 → 42883 undefined_function, 미구현 → 0A000 feature_not_supported, 제약 위반 → 23xxx, 설정 오류 → 55000 (b) .expect() 를 사용자 대면 경로에서 제거하고 error! + 원인 메시지로 대체 (c) 컴파일 SQL 노출을 og.debug_sql 설정으로 게이팅 (d) audit() 를 실패 경로에서도 호출하도록 run_read/run_write를 Result 반환으로 리팩터 (e) 세 계층의 문자열 매칭을 SQLSTATE 기반으로 교체 |
클라이언트 재시도 로직 가능, 정보 노출 축소, 관측 가능성 회복 | (a)는 광범위한 수정 — 모듈 단위로 점진 적용 가능. (e)는 (a)에 의존하므로 순서 필수 |
| API-32 | 버전 호환·폐기 경로가 전혀 없음 | Med | ontological_version() 이 env!("CARGO_PKG_VERSION") 을 그대로 반환 (engine/src/lib.rs:40), 현재 0.1.0 (engine/Cargo.toml:3) · engine/sql/ 에 마이그레이션 스크립트 없음 — bootstrap.sql(extension_sql_file!(..., bootstrap))과 access.sql(..., finalize) 두 개뿐 (engine/src/lib.rs:23) · og_catalog.schema_version 은 사용자 그래프 스키마 버전이지 확장 버전이 아님 (catalog/labeling.rs bump_schema_version) · engine/ontological.control 은 default_version = '0.1.0' 만 선언하고, ontological--X--Y.sql 형식의 업그레이드 스크립트가 저장소에 하나도 없음(find . -name '*--*.sql' → 0건) · 폐기 표시(@deprecated, COMMENT ON FUNCTION)가 하나도 없음 |
ALTER EXTENSION ontological UPDATE 경로가 없다. 함수 시그니처를 바꾸면 기존 사용자가 재설치해야 하고, 어떤 함수가 사라질지 알릴 수단이 없다. 클라이언트는 ontological_version() 문자열 비교 외에 기능 탐지 수단이 없다 |
(a) ontological--0.1.0--0.2.0.sql 형태의 업그레이드 스크립트 관례 도입 (b) og_capabilities() RETURNS jsonb 를 추가해 기능별 플래그를 노출 ({"cypher_union": false, "sparql": false, "typeql_params": false, ...}) — 클라이언트가 버전 문자열이 아니라 기능으로 분기 (c) 폐기 예정 함수에 COMMENT ON FUNCTION ... IS 'DEPRECATED: use X instead; removal in 0.3' 부착 (d) 본 문서군의 각 시그니처 변경 제안(API-03, API-08, API-11, API-15)에 대해 "구 함수 유지 + 신 함수 추가 + 2 마이너 후 제거" 창을 정의 |
확장을 업그레이드 가능한 소프트웨어로 만듦. 위 개선안 다수가 이 항목 없이는 파괴적 변경이 됨 | (a)는 기존 설치본의 마이그레이션 경로를 처음부터 만들어야 함. (b)는 유지 부담 — 기능 플래그가 코드와 어긋나면 API-10과 같은 문제 재발 |
2. 우선순위 제안 (의존 관계 포함)
API-32 (버전/폐기 경로) ──┐
├─→ 시그니처를 바꾸는 모든 항목의 전제
API-31 (SQLSTATE) ──┼─→ API-10, API-28(d), API-30(b) 의 전제
│
API-01 (UNION) ────┘ ← 전제 없이 즉시 가능 (거부만 추가)
API-29 (Studio 인증) ──── ← 전제 없이 즉시 가능
API-23 (데이터 손실) ──── ← 전제 없이 즉시 가능 (force 인자)
즉시 착수 가능(파괴적 변경 없음): API-01(거부만), API-05(문서+검증), API-06, API-09, API-12, API-16, API-19(거부만), API-20(NOTICE), API-22(문서), API-24, API-25, API-27, API-29, API-23(force 인자 추가)
API-32 이후: API-03, API-08, API-11, API-15, API-30(a)
API-31 이후: API-10, API-28(d), API-30(b)
3. 코드와 docs/api.md / docs/cypher.md 사이의 불일치 (정오표)
| # | 문서 위치 | 문서가 말하는 것 | 코드가 하는 것 | 관련 ID |
|---|---|---|---|---|
| 1 | docs/cypher.md:249 | og_explain_error(…, 'RETURN nosuchfunction(a)') → "code": "UNSUPPORTED_SYNTAX" |
classify()가 "unknown function"을 먼저 검사 → "UNKNOWN_FUNCTION" (agent/mod.rs:294) |
API-10 |
| 2 | docs/cypher.md:295 | ERROR: unknown label 'Persn' in graph 'social'. did you mean: Person |
그런 ERROR가 코드에 없음. NOTICE: label 'Persn' does not exist in graph '…' — matching nothing. did you mean: … (catalog/types.rs:168) |
API-10 |
| 3 | docs/cypher.md:237 | UNION — "parsed, not compiled" |
맞지만 조용히 첫 절반만 반환한다는 사실이 없음 (compile.rs에 union 참조 0건) |
API-01 |
| 4 | docs/api.md:139 | "Scores are normalised so higher is always better" | l2는 원 거리 그대로 (vector/mod.rs:123), og_vector_search_exact는 어떤 메트릭에서도 정규화 없음 (vector/mod.rs:430) |
API-15 |
| 5 | docs/api.md:178 | og_apply_role — "per-role statement_timeout, work_mem, read-only, row caps" |
max_rows는 SET og.max_rows 만 하고 읽는 코드가 없음 (agent/mod.rs:437) |
API-21 |
| 6 | docs/api.md:113 | og_typeql(graph, query, params = '{}') |
인자명이 _params이고 본문이 사용하지 않음 (typeql/mod.rs:52) |
API-11 |
| 7 | docs/api.md:132 | og_similar(graph, id, prop, k = 10) |
graph가 무시됨 — let _ = graph; (vector/mod.rs:173) |
API-18 |
| 8 | docs/api.md:134 | og_vector_search_exact(graph, type, prop, query, k) |
k의 기본값이 10인데 문서에 기본값 표기 없음 (vector/mod.rs:417). 반환에 entity 컬럼이 없다는 점도 미기재 |
API-15 |
| 9 | docs/api.md:133 | og_hybrid_search — "RRF fusion of vector rank and graph proximity" |
관계 타입에 동작하지 않음(노드 뷰 고정, vector/mod.rs:247). 그래프 근접이 og_vlp(…, 0, 3) 하드코딩 (vector/mod.rs:253) |
API-19 |
| 10 | docs/api.md 전체 | 함수 목록 | og_apoc_meta_schema, og_cypher_stats, og_genai_encode, og_set_setting, ontological_version 5개 누락. access.sql의 og_subtype_ids, og_capture_history 도 누락 |
— |
| 11 | docs/cypher.md:87 | 함수 표 | elementId, element_at, similarity(별칭), upper/lower(별칭) 누락 |
API-09 |
| 12 | docs/cypher.md:238 | FOREACH — "rejected at parse time" |
맞으나 메시지가 FOREACH를 지명하지 않음 — foreach가 키워드 목록에 없어(lexer.rs:17) expected a clause keyword 가 됨 |
API-09 |
| 13 | docs/api.md:148 | og_enable_rls(graph, type, policy_expr) — "row-level security across the hierarchy" |
맞으나 policy_expr가 물리 컬럼 이름(p_*)을 써야 한다는 점과 SQL로 보간된다는 점이 미기재 |
API-05 |
| 14 | docs/api.md:162 | og_dump_rdf(graph, format = 'turtle') |
포맷을 검증하지 않아 오타가 조용히 Turtle이 됨 (adapters/rdf.rs:697) |
API-24 |
| 15 | docs/cypher.md:142 | db.awaitIndex, db.awaitIndexes, db.clearQueryCaches — "accepted, do nothing" |
맞으나 NO_OPS 목록에 db.resamplindex 라는 오타가 있어 실제 Neo4j 이름 db.resampleIndex 는 거부됨 (compat/procs.rs:78) |
API-27 |
4. 이 문서를 갱신해야 하는 시점
- 새
#[pg_extern]함수 추가/제거 → 00_index.md 와 본 문서 classify()또는Failure::from_pg()변경 → 11_errors.md 와 §1의 API-10, API-31portal/server/index.js의 라우트 변경 → 10_studio_http_api.md 와 API-29, API-30docs/api.md/docs/cypher.md수정 → §3 정오표 항목 제거
5. 관련 문서
- 함수 색인 → 00_index.md
- 오류 체계 → 11_errors.md
- Cypher 경계 → 03_cypher.md
- Studio 계약 → 10_studio_http_api.md