그래프 · 타입 · 프로퍼티 · 역할 DDL
이 문서가 답하는 질문
- 그래프와 타입을 어떻게 만들고 지우는가?
- 프로퍼티를 선언하면 물리적으로 무슨 일이 일어나는가?
role과rule은 무엇을 강제하는가?- 서브타입 판정은 왜 재귀 없이 상수 시간인가?
- DDL 함수가 실패하는 정확한 조건은?
1. 사실 — DDL이 만드는 물리 구조
타입 하나를 선언하면 다음이 동시에 생성된다 (engine/src/catalog/types.rs:410).
| 산출물 | 형태 | 조건 |
|---|---|---|
| 카탈로그 행 | og_catalog.type |
항상 |
| 물리 저장 테이블 | 엔티티 og_data.n_<type_id>, 관계 og_data.e_<type_id> |
is_abstract = false 이고 kind <> 'attribute' |
| 별칭 뷰 | 타입 이름을 그대로 쓴 뷰 | 저장 테이블이 생길 때 |
| 인덱스 | 관계 테이블에 (src), (dst) |
kind = 'relation' |
| id 할당기 행 | og_data.og_id_alloc |
항상 |
| 구간 라벨 재계산 | og_catalog.type_label (lft/rgt) |
항상 (relabel_graph) |
엔티티 테이블의 기본 형태:
CREATE TABLE og_data.n_45 (id int8 PRIMARY KEY, __ext jsonb)
관계 테이블:
CREATE TABLE og_data.e_51 (id int8 PRIMARY KEY, src int8 NOT NULL,
dst int8 NOT NULL, __ext jsonb)
결정(Decision): 물리 테이블 이름을 n_<type_id>로 두고 사람이 읽을 이름은 뷰로 제공한다. 타입 이름 변경이 상수 시간이 되고, 식별자가 이동하지 않는다. 비용은 \dt에 n_45가 보인다는 것 — 그래서 뷰를 만든다 (engine/src/catalog/types.rs:422 주석).
2. 그래프
og_create_graph(name text) RETURNS int4
정의: engine/src/catalog/types.rs:300 · 휘발성: 기본값(VOLATILE) · 병렬: 기본값(PARALLEL UNSAFE)
무엇을 하는가: 그래프 네임스페이스를 만들고 graph_id를 돌려준다. 이미 있으면 기존 id를 반환한다(멱등).
인자
| 이름 | 타입 | 필수/선택 | 기본값 | 설명 |
|---|---|---|---|---|
name |
text |
필수 | — | 그래프 이름. 전역 유일 |
반환
| 컬럼 | 타입 | NULL 가능 | 설명 |
|---|---|---|---|
| (스칼라) | int4 |
아니오 | og_catalog.graph.graph_id |
예제
SELECT og_create_graph('meeting');
실패 조건: 실질적으로 없음 — 존재하면 조회해서 반환한다 (engine/src/catalog/types.rs:302). 카탈로그 INSERT가 실패하면 graph insert failed 패닉이 ERROR로 올라온다.
og_drop_graph(name text) RETURNS void
정의: engine/src/catalog/types.rs:321 · 휘발성: 기본값(VOLATILE) · 병렬: 기본값
무엇을 하는가: 그래프에 속한 모든 타입의 저장 테이블을 DROP TABLE ... CASCADE 하고 og_catalog.graph 행을 지운다.
인자
| 이름 | 타입 | 필수/선택 | 기본값 | 설명 |
|---|---|---|---|---|
name |
text |
필수 | — | 그래프 이름 |
반환: 없음(void).
예제
SELECT og_drop_graph('meeting');
실패 조건
- 그래프가 없으면:
graph '<name>' does not exist(engine/src/catalog/types.rs:118). - 저장 테이블 DROP 실패 시
drop table failed패닉.
주의(확인된 동작): og_data.og_node / og_data.og_edge / og_data.og_adj의 잔여 행은 삭제하지 않는다 — og_drop_type은 지우지만 (engine/src/catalog/types.rs:704), og_drop_graph는 저장 테이블과 graph 행만 지운다 (engine/src/catalog/types.rs:334). og_check_integrity()의 orphan_node가 남을 수 있다. → 12_improvements_api.md API-07.
3. 타입
og_create_type(graph text, name text, kind text, parents text[] DEFAULT '{}', is_abstract bool DEFAULT false) RETURNS int4
정의: engine/src/catalog/types.rs:348 · 휘발성: 기본값 · 병렬: 기본값
무엇을 하는가: 타입을 선언하고 저장 테이블·별칭 뷰·id 할당기를 만든 뒤 계층 라벨을 재계산한다.
인자
| 이름 | 타입 | 필수/선택 | 기본값 | 설명 |
|---|---|---|---|---|
graph |
text |
필수 | — | 그래프 이름 |
name |
text |
필수 | — | 타입 이름 (그래프 내 유일) |
kind |
text |
필수 | — | entity|e|node / relation|r|rel|edge / attribute|a|attr (engine/src/catalog/types.rs:374) |
parents |
text[] |
선택 | '{}' |
상위 타입 이름 목록. 두 개 이상이면 다중 상속 |
is_abstract |
bool |
선택 | false |
true면 저장 테이블을 만들지 않고 인스턴스화 불가 |
반환
| 컬럼 | 타입 | NULL 가능 | 설명 |
|---|---|---|---|
| (스칼라) | int4 |
아니오 | type_id |
예제 (실제 examples/demo.sql:19)
-- Abstract root: never instantiated, but queryable.
SELECT og_create_type('default', 'Work', 'entity', ARRAY[]::text[], true);
SELECT og_create_type('default', 'Film', 'entity', ARRAY['Work']);
SELECT og_create_type('default', 'AnimatedFilm', 'entity', ARRAY['Film']);
SELECT og_create_type('default', 'ACTED_IN', 'relation');
실패 조건
| 입력 | 오류 메시지 | 위치 |
|---|---|---|
| 그래프 없음 | graph '<g>' does not exist |
types.rs:118 |
| 같은 이름 타입 존재 | type '<name>' already exists in graph '<graph>' |
types.rs:372 |
kind가 목록 밖 |
unknown type kind '<k>' (entity | relation | attribute) |
types.rs:378 |
| 부모 타입 없음 | type '<p>' does not exist. did you mean: … |
types.rs:135 |
| 부모 종류 불일치 | type '<name>' (<kind>) cannot inherit from '<p>' of kind '<pk>' |
types.rs:387 |
type_id가 18비트 초과 |
type id <n> out of range (0..262143) |
engine/src/id.rs:39 |
필수 규칙: parents의 부모는 같은 kind 여야 한다. 엔티티가 관계를 상속할 수 없다.
og_drop_type(graph text, name text, cascade bool DEFAULT false) RETURNS void
정의: engine/src/catalog/types.rs:685 · 휘발성: 기본값 · 병렬: 기본값
무엇을 하는가: 타입과 모든 서브타입을 지운다. 인스턴스가 남아 있으면 cascade => true 없이는 거부한다(spec 002 FR-024).
인자
| 이름 | 타입 | 필수/선택 | 기본값 | 설명 |
|---|---|---|---|---|
graph |
text |
필수 | — | 그래프 이름 |
name |
text |
필수 | — | 타입 이름 |
cascade |
bool |
선택 | false |
true면 인스턴스가 있어도 삭제 |
반환: 없음.
삭제되는 것 (engine/src/catalog/types.rs:700): 서브타입별 저장 테이블(DROP TABLE ... CASCADE), og_data.og_node, og_data.og_edge, og_data.og_adj(etype 기준), og_catalog.type 행. 마지막에 relabel_graph(gid)로 구간 라벨을 재계산한다.
예제
SELECT og_drop_type('meeting', 'Reservation', cascade => true);
실패 조건
- 타입 없음 →
type '<name>' does not exist. did you mean: … - 인스턴스 존재 +
cascade = false→type '<name>' has <n> instance(s) (including subtypes). pass cascade => true to remove them(types.rs:698)
4. 프로퍼티
og_add_property(graph text, type_name text, prop text, data_type text, required bool DEFAULT false, is_key bool DEFAULT false) RETURNS void
정의: engine/src/catalog/types.rs:510 · 휘발성: 기본값 · 병렬: 기본값
무엇을 하는가: 프로퍼티를 선언하고, 해당 타입과 모든 서브타입의 저장 테이블에 실제 컬럼을 추가한다.
인자
| 이름 | 타입 | 필수/선택 | 기본값 | 설명 |
|---|---|---|---|---|
graph |
text |
필수 | — | 그래프 이름 |
type_name |
text |
필수 | — | 타입 이름 |
prop |
text |
필수 | — | 프로퍼티 이름(질의에서 쓰는 이름) |
data_type |
text |
필수 | — | 아래 표의 선언 타입 |
required |
bool |
선택 | false |
NOT NULL 부여 |
is_key |
bool |
선택 | false |
고유 인덱스 부여 |
지원하는 data_type (engine/src/catalog/types.rs:13)
| 선언값(대소문자 무관) | 물리 컬럼 타입 |
|---|---|
string | text | str |
text |
int | integer | int4 |
int4 |
long | bigint | int8 |
int8 |
float | double | float8 |
float8 |
real | float4 |
float4 |
bool | boolean |
bool |
datetime | timestamptz | timestamp |
timestamptz |
date |
date |
uuid |
uuid |
numeric | decimal |
numeric |
json | jsonb |
jsonb |
text[] | string[] |
text[] |
int[] | bigint[] |
int8[] |
vector(N) (N은 숫자) |
vector(N) — pgvector |
그 외는 오류: unsupported property type '<decl>'. supported: string, int, long, float, bool, datetime, date, uuid, numeric, jsonb, text[], int[], vector(N) (types.rs:38).
물리 컬럼 이름 규칙 (engine/src/catalog/types.rs:53): p_ 접두사 + 소문자화, 영숫자·_ 외의 문자는 _로 치환. 예: beginTime → p_begintime, isbn-13 → p_isbn_13.
⚠️ 이 규칙은 대소문자를 지운다.
userName과username이 같은 컬럼p_username으로 충돌한다 → 12_improvements_api.md API-04.
부수 효과 (전부 확인됨)
- 서브타입 전체에
ALTER TABLE ... ADD COLUMN IF NOT EXISTS(types.rs:550). required면ALTER COLUMN ... SET NOT NULL.- 선언 이전에
__extjsonb에 들어가 있던 값들을 컬럼으로 이동시키고__ext에서 제거 (types.rs:561). "먼저 쓰고 나중에 인덱스"라는 스키마리스 그래프의 통상적 순서를 지원하기 위한 것. is_key면CREATE UNIQUE INDEX IF NOT EXISTS uq_<sub>_<col>.- 서브타입의 별칭 뷰를 재생성 (뷰는 생성 시점 컬럼 목록을 고정하므로).
og_catalog.schema_version증가.
예제 (examples/meeting-rooms/schema.sql:12)
SELECT og_add_property('meeting', 'MeetingRoom', 'name', 'string', true, true);
SELECT og_add_property('meeting', 'MeetingRoom', 'seats', 'int', false, false);
SELECT og_add_property('meeting', 'Reservation', 'begin_time', 'datetime', true, false);
-- named-argument form also works
SELECT og_add_property('default', 'Person', 'name', 'string', required => true);
실패 조건
| 입력 | 오류 |
|---|---|
| 그래프/타입 없음 | graph '…' does not exist / type '…' does not exist. did you mean: … |
알 수 없는 data_type |
unsupported property type '…' |
required = true 인데 기존 인스턴스 존재 |
cannot add required property '<prop>' to '<type>': <n> existing instance(s) would violate it. add it as optional, backfill, then tighten. (types.rs:532) |
| 같은 프로퍼티 재선언 | property insert failed — og_catalog.property (type_id, name) 유일 제약 위반이 패닉으로 표면화 (types.rs:545) |
__ext 값 이동 실패(타입 캐스트 불가) |
failed to move existing '<prop>' values into its column: <e> (types.rs:569) |
og_create_index(graph text, type_name text, prop text) RETURNS void
정의: engine/src/catalog/types.rs:603 · 휘발성: 기본값 · 병렬: 기본값
무엇을 하는가: 선언된 프로퍼티 컬럼에 B-tree 인덱스를 계층 전체에 만든다(spec 001 FR-016).
인자
| 이름 | 타입 | 필수/선택 | 기본값 | 설명 |
|---|---|---|---|---|
graph |
text |
필수 | — | 그래프 이름 |
type_name |
text |
필수 | — | 타입 이름 |
prop |
text |
필수 | — | 프로퍼티 이름 |
반환: 없음.
생성되는 인덱스 이름은 ix_<sub_type_id>_<column_name> 이며 CREATE INDEX IF NOT EXISTS이므로 재실행이 안전하다.
예제
SELECT og_create_index('default', 'Person', 'name');
실패 조건
- 그래프/타입 없음 → 위와 동일.
- 프로퍼티가 선언되어 있지 않아도 오류가 나지 않는다.
column_name(prop)은 이름만 계산하므로 존재하지 않는 컬럼을 인덱싱하려다index creation failed패닉이 난다(types.rs:611). CypherCREATE INDEX경로는 이 문제를ensure_property로 미리 막지만 (engine/src/compat/ddl.rs:132), SQL 함수 직접 호출 경로에는 그 보호가 없다 → 12_improvements_api.md API-06.
5. 역할(Role)과 규칙(Rule)
og_add_role(graph text, rel_type text, role text, player_type text, ordinal int4, card_min int4 DEFAULT 0, card_max int4 DEFAULT NULL) RETURNS void
정의: engine/src/catalog/types.rs:625 · 휘발성: 기본값 · 병렬: 기본값
무엇을 하는가: 관계 타입에 이름 붙은 참여 슬롯을 선언한다(spec 002 FR-004..FR-006). ON CONFLICT (rel_type_id, name) DO UPDATE이므로 재선언은 갱신이다.
인자
| 이름 | 타입 | 필수/선택 | 기본값 | 설명 |
|---|---|---|---|---|
graph |
text |
필수 | — | 그래프 이름 |
rel_type |
text |
필수 | — | 관계 타입 이름 |
role |
text |
필수 | — | 역할 이름 |
player_type |
text |
필수(NULL 허용) | — | 이 역할을 맡을 수 있는 타입. NULL이면 제약 없음 |
ordinal |
int4 |
필수 | — | 0 = 출발(src), 1 = 도착(dst), 2 이상 = n-ary 참여자 |
card_min |
int4 |
선택 | 0 |
최소 카디널리티 (카탈로그에 기록됨) |
card_max |
int4 |
선택 | NULL |
최대 카디널리티 (카탈로그에 기록됨) |
반환: 없음.
무엇을 강제하는가 (확인됨): ordinal ∈ {0, 1} 이고 player_type_id IS NOT NULL인 역할만 og_create_edge 쓰기 시점에 검사된다 (engine/src/storage/mod.rs:455 validate_roles). card_min/card_max는 카탈로그에 저장만 되고 이 경로에서 강제되지 않는다 — TypeQL 쓰기 경로는 별도로 카디널리티 상한을 검사한다 (engine/src/typeql/write.rs:278).
예제 (examples/demo.sql:38)
SELECT og_create_type('default', 'ACTED_IN', 'relation');
SELECT og_add_role('default', 'ACTED_IN', 'actor', 'Person', 0);
SELECT og_add_role('default', 'ACTED_IN', 'production', 'Work', 1);
이후 다음 쓰기는 거부된다:
ERROR: role 'actor' of relation 'ACTED_IN' requires a 'Person', got 'Film'
실패 조건
rel_type이 관계 타입이 아님 →'<rel_type>' is not a relation type; roles only exist on relations(types.rs:638)player_type이 존재하지 않음 →type '…' does not exist. did you mean: …
og_add_rule(graph text, rel_type text, characteristic text, target_type text DEFAULT NULL) RETURNS void
정의: engine/src/catalog/types.rs:657 · 휘발성: 기본값 · 병렬: 기본값
무엇을 하는가: 관계 특성을 선언한다(spec 002 FR-027).
인자
| 이름 | 타입 | 필수/선택 | 기본값 | 설명 |
|---|---|---|---|---|
graph |
text |
필수 | — | 그래프 이름 |
rel_type |
text |
필수 | — | 관계 타입 이름 |
characteristic |
text |
필수 | — | transitive / symmetric / reflexive / inverse (대소문자 무관) |
target_type |
text |
선택 | NULL |
inverse일 때 필수 — 역관계가 되는 관계 타입 |
반환: 없음. ON CONFLICT DO NOTHING이므로 멱등.
예제
SELECT og_add_rule('default', 'COLLABORATED_WITH', 'symmetric');
SELECT og_add_rule('default', 'PART_OF', 'transitive');
SELECT og_add_rule('default', 'PARENT_OF', 'inverse', 'CHILD_OF');
실패 조건
- 목록 밖의 특성 →
unknown characteristic '<c>' (transitive|symmetric|reflexive|inverse)(types.rs:668) inverse인데target_type이NULL→characteristic 'inverse' requires a target relation type(types.rs:671)
6. 상속 조회 — 구간(nested-set) 라벨
결정(Decision): 서브타입 판정은 재귀 워크가 아니라 og_catalog.type_label의 lft/rgt 구간 비교 한 번이다 (engine/src/catalog/labeling.rs:192). 이 세 함수는 Cypher 컴파일러가 라벨 술어를 SQL로 펼칠 때 그대로 방출하는 것들이다.
og_subtypes(type_id int4) RETURNS int4[]
정의: engine/src/catalog/labeling.rs:192 · 휘발성: STABLE · 병렬: PARALLEL SAFE, STRICT
무엇을 하는가: 자기 자신과 모든 후손 타입 id를 반환한다.
인자
| 이름 | 타입 | 필수/선택 | 기본값 | 설명 |
|---|---|---|---|---|
type_id |
int4 |
필수 | — | 루트 타입 id. NULL이면 STRICT이므로 NULL 반환 |
반환
| 컬럼 | 타입 | NULL 가능 | 설명 |
|---|---|---|---|
| (스칼라) | int4[] |
NULL 입력 시 NULL |
자기 자신 포함. 순서 보장 없음 |
예제
SELECT og_subtypes(og_type_id('default', 'Work'));
-- {12,13,14,15} -- Work, Film, AnimatedFilm, Series
og_supertypes(type_id int4) RETURNS int4[]
정의: engine/src/catalog/labeling.rs:212 · 휘발성: STABLE · 병렬: PARALLEL SAFE, STRICT
무엇을 하는가: 자기 자신과 모든 조상 타입 id를 반환한다. Cypher labels(n)이 이 함수를 사용해 상위 체인 전체를 라벨 목록으로 돌려준다(engine/src/cypher/compile.rs:1475).
og_is_subtype(sub int4, sup int4) RETURNS bool
정의: engine/src/catalog/labeling.rs:232 · 휘발성: STABLE · 병렬: PARALLEL SAFE, STRICT
무엇을 하는가: sub ⊑ sup 판정. 존재하지 않는 id를 넣으면 false.
예제
SELECT og_is_subtype(og_type_id('default','AnimatedFilm'),
og_type_id('default','Work')); -- t
og_relabel(graph_id int4) RETURNS void
정의: engine/src/catalog/labeling.rs:247 · 휘발성: 기본값 · 병렬: STRICT
무엇을 하는가: 구간 라벨을 강제로 재계산한다. 진단·복구용이며 정상 DDL 경로는 자동으로 호출한다.
주의: 인자가 그래프 이름이 아니라 graph_id(int4) 다 — 다른 DDL 함수와 유일하게 다른 관례 → 12_improvements_api.md API-02.
SELECT og_relabel((SELECT graph_id FROM og_catalog.graph WHERE name = 'default'));
7. 인트로스펙션 뷰
SELECT * FROM og_type_view WHERE graph = 'default';
SELECT * FROM og_property_view WHERE graph = 'default' AND type_name = 'Person';
SELECT * FROM og_role_view WHERE graph = 'default';
컬럼 정의는 00_index.md §4.10 참조.
8. 권한 — og_grant / og_revoke
이 절은 SEC-01~SEC-17 수정으로 추가되었다. 설계 근거는 ADR-025.
확장은 설치 시 자신의 모든 og_* 함수에서 PUBLIC 의 EXECUTE 를 회수한다. 따라서 설치 직후에는 소유자만 쓸 수 있고, 배분은 명시적으로 한다.
-- 롤은 직접 만든다. 확장은 만들어 주지 않는다.
CREATE ROLE analytics LOGIN PASSWORD '...';
SELECT og_grant('analytics', 'read'); -- 조회·인트로스펙션·통계
SELECT og_grant('analytics', 'write'); -- + 노드/엣지 DML, 임베딩
SELECT og_grant('analytics', 'admin'); -- + DDL, 설정, 매핑, RLS
SELECT og_revoke('analytics'); -- 전부 회수
| 함수 | 인자 | 동작 |
|---|---|---|
og_grant(role, level) |
level 기본 'read' |
스키마 USAGE, 테이블·시퀀스 권한, 해당 레벨의 함수 EXECUTE 를 부여하고 og_catalog.grantee 에 기록 |
og_revoke(role) |
— | og_catalog/og_data 전체와 확장 함수 전체에서 회수하고 기록 삭제 |
레벨은 중첩된다: read ⊂ write ⊂ admin.
롤이 없으면 오류다. 롤은 클러스터 전역이고 DROP EXTENSION 이후에도 남으므로 확장이 만들지 않는다.
기록해 두는 이유. 저장 테이블은 구체 타입마다 런타임에 생기므로, 지금 준 권한이 아직 존재하지 않는 테이블에 닿아야 한다. ALTER DEFAULT PRIVILEGES 는 객체를 만든 롤 기준이라 이 경우에 동작하지 않는다 — 이 테이블들은 og_create_type 을 호출한 아무나가 만든다. 그래서 og_catalog.grantee 에 남기고 생성 시점에 재부여한다.
읽기 롤이 og_cypher 를 가지는 것에 대해
og_cypher 는 read 레벨에 포함된다. Cypher 로 CREATE/DELETE 를 쓸 수 있는데도 그렇다. 컴파일된 질의는 평범한 테이블을 읽고 쓰기 때문이다. og_data 에 SELECT 만 가진 롤은 쓰기 문장 자체에서 권한 오류를 받는다. 경계는 함수가 아니라 테이블이며, 그쪽이 우리가 질의를 정확히 파싱하는 데 의존하지 않는다.
쓰기 롤의 한계
선언되지 않은 프로퍼티를 쓰면 컬럼 승격이 일어나는데, 이건 DDL 이므로 write 롤에서는 실패한다. 스키마 변경을 admin 쪽에 남기는 건 의도한 선이지만, 현재 declare_new_props 는 실패를 is_ok() 로만 확인하므로 SPI 오류가 트랜잭션을 중단시키는 경로가 남아 있다. 미해결 사항으로 10_fixed.md 4절에 기록돼 있다.
9. 금지 / 필수
- 필수: 프로퍼티는 인덱스를 만들기 전에
og_add_property로 선언할 것.og_create_index는 컬럼 존재를 확인하지 않는다. - 필수:
required => true는 인스턴스가 하나도 없을 때 붙일 것. 이후에는 "선택으로 추가 → 백필 → 조인다" 순서를 따를 것(오류 메시지가 이 순서를 안내한다). - 금지:
og_catalog.type/og_catalog.property를 직접 수정하지 말 것. 물리 컬럼과 구간 라벨이 함께 움직이지 않는다. - 금지: 추상 타입(
is_abstract => true)에 인스턴스를 만들려 하지 말 것 — 저장 테이블 자체가 없다('<name>' is abstract and cannot be instantiated). - 필수: 프로퍼티 이름을 대소문자만 다르게 두지 말 것 — 물리 컬럼이 충돌한다(§4).
10. 관련 문서
- 데이터 쓰기 → 02_data_dml.md
- Neo4j
CREATE INDEX/CREATE CONSTRAINT경로 → 09_neo4j_compat.md - TypeQL
define→ 04_typeql.md - 오류 체계 → 11_errors.md