확장 함수 작성
확장 함수는 C++로 작성되며 VEF에 등록됩니다. VDF, 타입, 확장 빌더 API에 액세스하려면 단일 헤더를 포함합니다:예외 안전성
예외를 가두는 것은 확장의 책임입니다. SDK는 사용자의 구현을try/catch로
감싸지 않으며 서버도 마찬가지이므로, 엔트리 포인트를 벗어난 예외는 그 위
어디에도 핸들러가 없는 상태로 C ABI 경계를 넘어 스택을 풀고 서버 프로세스가
종료됩니다. 일반적인 SQL 입력이 이 상황에 도달합니다: std::stoi()는 숫자가
아닌 문자열에서 예외를 던지고, nlohmann::json::dump()는 유효한 UTF-8이 아닌
텍스트에서 예외를 던집니다.
등록하는 모든 엔트리 포인트의 본문을 감싸세요: VDF, 집계 함수, prerun 및
postrun 훅, 그리고 사용자 정의 타입의 연산.
const std::exception &에 더해 ...도 잡으세요: 의존성이 std::exception을
상속하지 않는 타입을 던질 수 있으며, 그런 예외도 여전히 서버를 종료시킵니다.
warning()과 error() 중 무엇을 사용할지는 다른 모든 실패와 동일한 기준으로
선택합니다 — 문장이 계속 진행되어야 하는 잘못된 입력에는 warning(), 계속
실행이 안전하지 않은 경우에는 error()입니다.
인자 및 결과 타입
VDF 매개변수 및 결과는 타입 안전 인자 및 결과 타입으로 전달됩니다. 프레임워크는 함수 시그니처에서 이를 감지하고 자동으로 적응합니다 —make_func 등록 구문은 변경되지 않습니다.
인자 타입: IntArg, RealArg, StringArg, CustomArg — 각각
is_null() 및 value()를 제공합니다. 매개변수화된 사용자 정의 타입의 경우,
CustomArgWith<P>는 캐시된 구문 분석된 매개변수 구조체를 반환하는 params()
액세서를 추가합니다(참조: 매개변수화된 타입).
결과 타입: IntResult, RealResult, StringResult, CustomResult
— 각각 set_null(), warning(msg), error(msg)를 제공합니다. 스칼라
결과는 추가로 set(value)를 제공합니다. 버퍼 결과는 buffer() 및
set_length(len)을 제공합니다. StringResult는 추가로
set(std::string_view)를 제공하여, 뷰에서 최대 buffer().size() 바이트를
복사하고 길이를 한 번에 설정합니다. 매개변수화된 사용자 정의 타입의 경우,
CustomResultWith<P>는 params() 액세서를 추가합니다.
스팬 타입: 바이트 기반 인자 및 결과 타입의 value() 및 buffer()는
vsql::Span<T>를 반환합니다 — data(), size(), empty(),
begin()/end(), operator[]를 갖는, 연속된 T 구간에 대한 소유하지 않는
뷰입니다. C++20에서는 std::span<T>의 별칭이며, C++17에서는 이 SDK가
최소한의 호환 구현을 제공하므로 동일한 코드가 두 표준에서 컴파일됩니다.
<villagesql/vsql.h>를 통해 사용할 수 있습니다.
warning(msg)는 행에 대해 SQL NULL을 반환하고 SQL 경고를 추가합니다. 그 NULL은 서버의 관점에서 일반적인 값이므로, 다른 모든 NULL과 같은 방식으로만 문장 오류가 됩니다: NOT NULL 열에 이를 쓰면 INSERT INTO t VALUES (NULL)과 마찬가지로 STRICT_TRANS_TABLES에서 실패합니다. NULL 허용 열에 쓰면 경고와 함께 성공합니다. 인코딩 함수의 구문 분석 불가능한 문자열과 같은 복구 가능한 잘못된 입력에는 warning(msg)를 사용합니다. 손상된 저장 데이터 또는 계속 실행이 안전하지 않은 조건에는 error(msg)를 사용합니다. 두 메시지 모두 필요한 경우 서버의 내부 오류 버퍼에 맞게 잘립니다.
스칼라 예제 — 두 정수를 더합니다:
StringResult 및 CustomResult의 경우, buffer()에 쓴 다음 작성된 바이트
수와 함께 set_length()를 호출합니다. buffer().size()는 최대 용량입니다.
사용자 정의 타입을 반환하는 VDF(.returns(MYTYPE))의 경우, 서버는
결과 버퍼를 해석된 반환 타입의 persisted_length에 맞춰 자동으로 크기
조정합니다 — 확장 작성자는 이 경우 함수 빌더에 .buffer_size(...)를 선언할
필요가 없습니다. prerun이 버퍼를 더 크게 늘리면 그 큰 크기가 유지됩니다.
예를 들어 이 덕분에 SVECTOR::from_string('[…1024 floats…]')이 결과 버퍼
공간이 부족해지는 일 없이 넓은 벡터를 인코딩할 수 있습니다.
동일한 확장 내에서 함수 간에 다른 스타일을 사용할 수 있습니다 — 각 함수의
스타일은 자체 시그니처에 따라 결정됩니다.
집계 VDF
집계 VDF는 각GROUP BY 그룹 내에서 여러 행에 걸쳐 상태를 누적하고 그룹당 단일
결과를 반환합니다. SQL SUM 또는 COUNT와 유사합니다. 등록하려면
make_aggregate_func<State, &result_fn>("name")을 사용합니다. State 타입은
그룹별 누적 버퍼입니다. prerun 및 postrun은 이를 할당하고 삭제하기 위해
자동으로 생성됩니다.
결과 함수는 void(const State&, ResultType) 시그니처를 가져야 하며,
ResultType은 IntResult, RealResult, StringResult, CustomResult,
또는 CustomResultWith<P> 중 하나입니다. out.set(value)를 호출하여 값을
반환하거나 out.set_null()을 호출하여 SQL NULL을 반환합니다.
.clear<>() 및 .accumulate<>()가 모두 필요합니다. 빌더는 이를 컴파일
타임에(build()를 통해) 강제하고, 서버는 INSTALL EXTENSION 시점에 다시
검증합니다 — clear는 상태를 재설정하고, accumulate는 행을 결합하며, 결과
함수는 최종 상태를 읽습니다.
make_aggregate_func<State, &result_fn>()은prerun및postrun을 자동 생성합니다(State를 값 초기화하고 삭제합니다)..clear<&fn>()은 사용자의void(State&)재설정 함수를 등록합니다..accumulate<&fn>()은 사용자의void(State&, TypedArgs...)결합 함수를 등록합니다.TypedArgs는 함수 시그니처에서 추론됩니다(IntArg,StringArg등)..accumulate<>()는 모든.param(TYPE)호출 뒤에 호출하세요: 빌더는 타입 인자 개수를 선언된 SQL 매개변수 개수와 컴파일 타임에 대조하므로, 더 먼저 호출하면 빌드에 실패합니다.- 결과 타입(
IntResult,RealResult등)은 결과 함수 시그니처에서 추론됩니다.
StringResult 집계 VDF는 텍스트를 반환합니다: 결과는 utf8mb4_bin 문자
집합 및 콜레이션을 보고하므로, 클라이언트는 이를 16진수가 아닌 문자로 표시합니다
— 스칼라 VDF STRING 경로와 동일합니다. 또한 .max_result_length(n)(선택적으로
활성화하는 개발 ABI 헤더(-DVSQL_USE_DEV_ABI=ON)와 프로토콜 4가 필요합니다)을
동일한 방식으로 존중하여, 구체화된 집계 결과(GROUP BY/DISTINCT 임시 테이블,
CREATE TABLE ... SELECT, 또는 UNION)의 크기를 조정하여 인자 너비에서
잘리지 않도록 합니다. 크기 조정 규칙 및 상한선은
사용자 정의 버퍼 크기를
참조하세요.
문장별 상태 (Prerun 및 Postrun)
일부 VDF는 단일 쿼리가 처리하는 모든 행에 걸친 상태를 필요로 합니다 — 호출 카운터, 캐시된 결과, 열린 리소스. prerun 훅에서 할당하고, VDF 본문에서 액세스하고, postrun 훅에서 해제합니다. 두 훅은 문장당 한 번 실행되며, VDF 본문은 행당 한 번 실행됩니다..prerun<&Hook>() 및 .postrun<&Hook>()으로 등록합니다. 필수 시그니처는
다음과 같습니다:
PrerunResult::set_user_data(void*)를 사용하여 상태를 저장하고,
PostrunArgs::delete_state<T>()를 사용하여 이를 해제합니다. prerun이
set_user_data(new T{})를 호출하면, postrun은 반드시
delete_state<T>()를 호출해야 합니다 — SDK는 자동 해제하지 않습니다.
PrerunArgs::type_at(i)는 행을 읽기 전에 각 인자의 선언된 SQL
타입을 노출합니다. 반환된 PrerunArgType의 조건식 is_int(), is_real(),
is_str(), is_custom()은 열 타입을 반영합니다. 이를 prerun에서 인자
타입을 검증하거나 PrerunResult::request_buffer_size(n)을 호출하여 결과
버퍼 크기를 조정하는 데 사용합니다.
가변 인자 VDF
가변 인자 VDF는 임의의 SQL 타입의 임의 개수의 인자를 수용합니다. func 빌더에.varargs()로 선언하며, 이는 .no_params() 및 .param(TYPE)과 상호
배타적입니다. 본문은 일반적인 고정 인자 타입 대신 vsql::VarArgs 인자를
받습니다.
프레임워크는 가변 인자 VDF의 인자 개수 또는 타입을 검증할 수 없습니다. 모든
가변 인자 등록은 잘못된 입력에 PrerunResult::error()를 호출하거나 결과
버퍼 크기를 조정하기 위해 PrerunResult::request_buffer_size(n)을 호출하는
prerun 훅과 짝을 이루어야 합니다.
범위-기반 for 루프로 인자를 반복합니다. 각 AnyArg 요소는 값을 읽기 전에
타입 검사를 요구합니다:
액세서를 사용하기 전에
is_null()을 확인하세요 — 네 가지 모두 NULL 인자에서 정의되지 않습니다.
PrerunArgType::custom_type()은 인자의 사용자 정의 타입 이름을
std::string_view로 반환합니다 — 사용자 정의 타입이 아닌 인자에서는 비어
있습니다. is_custom()만으로는 서버에 등록된 모든 사용자 정의 타입을
수용합니다. custom_type() 비교와 짝을 이루면 가변 인자 호출을 하나의 특정
타입으로 좁힐 수 있습니다:
해석 시점에 함수의 타입 매개변수 결정
서버의 내장 규칙은 보통 인자의 선언된 타입으로부터 반환 타입의 매개변수를 알아냅니다. 때로는 그럴 수 없습니다. 예를 들어tvector_concat(TVECTOR(M), TVECTOR(N))은 TVECTOR(M+N)을 반환합니다. 내장
규칙은 차원이 다른 두 TVECTOR 인자를 거부하며, 인자의 매개변수를 반환 타입으로
복사할 수는 있어도 더할 수는 없습니다. 이런 경우에는 func 빌더의
.bind_and_check_types<&Hook>()으로 다음 시그니처를 갖는
bind-and-check-types 훅을 등록합니다:
.bind_and_check_types()와 .varargs()는 동일한 함수에서 상호 배타적입니다 —
가변 인자 VDF에는 훅이 판단할 고정 인자 목록이 없습니다. 빌더 체인에서
.bind_and_check_types<&Hook>()보다 앞에 .varargs()를 선언하면 컴파일 타임
static_assert 실패가 발생합니다. 반대 순서로 선언하면 컴파일은 되지만, 서버가
로드할 때 확장이 거부됩니다: 등록은 모든 함수의 최종 빌드 상태를 순회하며 가변
인자이면서 bind 훅을 가진 첫 번째 함수를 다음과 같이 거부합니다:
vsql_tvector
예제 확장은 tvector_concat에 그러한 훅을 등록합니다:
vsql_tvector가 포함되지만, 릴리스 tarball에는
포함되지 않습니다.
확장 로드 및 언로드 훅
확장은 때때로 호출마다가 아니라 로드마다 한 번씩 수행해야 하는 작업이 있습니다 — CPU에 특화된 구현을 선택하거나, 자체 구성을 한 번 읽거나, VDF가 이후에 읽을 조회 테이블을 구축하는 작업입니다. 그 작업은 확장 빌더의.on_init<&Fn>()으로
등록하고, 대응하는 정리 작업은 .on_deinit<&Fn>()으로 등록합니다. 둘 다
void() 시그니처를 갖는 함수를 받으며, 반드시 함께 등록해야 합니다 — 하나만
선언하고 다른 하나를 선언하지 않으면 컴파일에 실패합니다. 다른 방법으로 짝이
맞지 않는 상태로 서버에 도달한 확장은 대신 로드 시 거부됩니다:
on_init은 로드마다 한 번 — INSTALL EXTENSION 시점, 그리고 서버가 시작될
때마다 — 서버가 확장을 검증하고 필요한 기능을 채운 후에 실행되므로, 등록된
시스템 변수는 이미 구성된 값을 보유하고 있습니다. 특히 시작 시에는 지속된 시스템
변수 값이 적용될 때까지 on_init이 지연되므로, SET PERSIST로 설정된 값 대신
컴파일된 기본값을 보는 일은 결코 없습니다. 서버가 거부한 확장에 대해서는 절대
실행되지 않습니다. on_deinit은 언로드 시점 — UNINSTALL EXTENSION 또는 서버
종료 시 — 에, 서버가 그 동일한 기능을 비우기 전에 실행되므로, on_init이 본 것과
동일한 라이브 상태를 봅니다.
INSTALL EXTENSION my_extension 후에 SELECT fast_sqrt(16)은 4를
반환합니다 — build_table이 이미 실행된 경우에만 도달할 수 있는 결과입니다.
VEF_GENERATE_REGISTRATION
VEF_GENERATE_REGISTRATION은 확장 등록을 수행하지만 extern "C" 엔트리
포인트를 정의하지 않는 내부 _vef_do_register() 헬퍼를 생성합니다.
vef_register 동작을 사용자 정의해야 할 때 사용합니다 — 예를 들어 테스트
빌드에서 등록 후 디스크립터를 패치하기 위해서입니다. 일반 확장의 경우 대신
VEF_GENERATE_ENTRY_POINTS를 사용합니다.
사용자 정의 타입 연산
전체 타입 연산 빌더 참조 — 인코딩, 디코딩, 비교, 해시, 내장 기본값, 매개변수화된 타입 — 는 타입 연산을 참조하세요.미리보기 기능
다음 VEF 기능은 선택적으로 활성화하는 미리보기 헤더로 사용할 수 있습니다. ABI 및 API는 여전히 활발히 개발 중입니다. 전체 참조는 미리보기 기능을 참조하세요.- 확장 시스템 변수 — 미리보기 기능 → 시스템 변수
- 확장 상태 변수 — 미리보기 기능 → 상태 변수
- 키링 액세스 — 미리보기 기능 → 키링 액세스
- 컬럼 저장 — 미리보기 기능 → 컬럼 저장
확장 등록 메타데이터 검사
INFORMATION_SCHEMA.EXTENSION_REGISTRATION은 로드된 각 확장의 인메모리 VEF
등록 구조체를 JSON 문서로 노출합니다. INSTALL EXTENSION 후 서버가 확장의
함수와 타입을 올바르게 해석했는지 확인하는 데 사용합니다.
참고 자료
- C++로 확장 만들기 — 종단간 빌드 단계, CMake 설정 및 설치
- C++ 테스트 — 로컬 개발 서버, MTR 및 실패 디버깅
- 타입 연산 — 인코딩, 디코딩, 비교, 해시, 매개변수화된 타입
- C++ API 참조 — VDF 계약, null 처리 및 버퍼 크기 조정
- 확장 아키텍처 — 라이프사이클, Victionary 캐싱, 성능 패턴 및 보안 모델

