Skip to main content
이 가이드는 VillageSQL 확장에 대한 VDF 구현 작성 및 회귀 테스트 실행을 다룹니다. 확장 만들기 가이드와 함께 사용되며, 종단간(end-to-end) 빌드 단계를 다룹니다.
VEF 프로토콜 3은 v0.0.4부터 안정적입니다. 프로토콜 4는 개발 중이며, 선택적으로 활성화하는 개발 ABI 헤더(-DVSQL_USE_DEV_ABI=ON)를 통해 사용할 수 있습니다. 이전 프로토콜 2로 빌드된 확장은 서버에서 거부되며 재빌드가 필요합니다.
VillageSQL 서버 자체에 기여하는 경우(확장 빌드가 아닌 경우), 소스에서 빌드하기를 참조하세요. 이는 mysql-test-run.pl을 직접 사용하여 테스트를 실행하는 전체 서버 개발 워크플로우를 다룹니다.

환경 설정

확장 개발 및 테스트를 위해 빌드된 VillageSQL 서버가 필요합니다. 서버 바이너리를 컴파일하기 위해 소스에서 복제 및 빌드하기 가이드를 따르세요. 빌드가 완료되면 villagesql CLI를 사용하여 로컬 개발 서버 인스턴스를 관리합니다. 모든 명령어는 VillageSQL이 설치된 디렉터리에서 실행해야 합니다.

로컬 개발 서버 시작

서버 인스턴스를 초기화하고 시작합니다:
초기화 시 루트 암호를 설정하려면:
다중 독립 인스턴스를 관리하려면 명령어 앞에 --dir <path>를 지정하거나, 현재 작업 디렉터리에 서버 디렉터리를 생성하려면 --here를 사용합니다:

확장 파일 관리

SQL을 통해 확장을 설치하기 전에 .veb 파일이 서버에 있어야 합니다. CLI는 서버의 lib/veb/ 디렉터리를 관리합니다:
init 이전에 lib/veb/에 배치된 .veb 파일은 자동으로 시드됩니다. 파일을 추가한 후 SQL을 통해 확장을 설치합니다:

확장 함수 작성

확장 함수는 C++로 작성되며 VEF에 등록됩니다. SDK 전체에 액세스하려면 단일 헤더를 포함합니다:
타입 안전 래퍼는 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 경고를 추가합니다. 엄격 모드(STRICT_TRANS_TABLES)에서는 MySQL이 INSERT/UPDATE에서 문장 오류로 승격시켜 error(msg)와 동일한 동작을 보입니다. 이는 구문 분석 불가능한 문자열이 인코딩 함수에서 발생하는 등 복구 가능한 잘못된 입력에 사용됩니다. 손상된 저장 데이터 또는 계속 실행이 안전하지 않은 조건에서는 error(msg)를 사용합니다. 두 메시지는 서버 내부 오류 버퍼에 맞게 필요한 경우 자동으로 잘립니다. 스칼라 예제 — 두 정수를 더합니다:
이진 예제 — 사용자 정의 타입 버퍼를 제자리에서 변환합니다:
StringResultCustomResult의 경우, buffer()에 쓴 후 set_length()로 작성된 바이트 수를 호출합니다. buffer().size()는 최대 용량입니다. VDF가 사용자 정의 타입을 반환하는 경우(returns(CUSTOM(MYTYPE))), 서버는 결과 버퍼를 해석된 반환 타입의 persisted_length에 맞춰 자동으로 크기 조정합니다 — 확장 작성자는 이 경우 함수 빌더에 .buffer_size(...)를 선언할 필요가 없습니다. prerun이 버퍼를 더 크게 늘리면 그 크기가 유지됩니다. 예를 들어, SVECTOR::from_string('[…1024 float…]')는 결과 래퍼가 공간 부족을 일으키지 않도록 넓은 벡터를 인코딩할 수 있습니다. 동일한 확장 내에서 함수 간에 다른 스타일을 사용할 수 있습니다 — 각 함수의 스타일은 자체 시그니처에 따라 결정됩니다.

집계 VDF

집계 VDF는 각 GROUP BY 그룹 내에서 행을 통해 상태를 누적하고 그룹당 단일 결과를 반환합니다. SQL SUM 또는 COUNT와 유사합니다. make_aggregate_func<State, &result_fn>("name")을 사용하여 등록합니다. State 타입은 그룹별 누적 버퍼입니다. prerunpostrun은 자동으로 생성되어 할당 및 삭제합니다. 결과 함수는 void(const State&, ResultWrapper) 시그니처를 가져야 하며, ResultWrapperIntResult, 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>()prerunpostrun을 자동 생성합니다(값 초기화 및 삭제).
  • .clear<&fn>()void(State&)vef_vdf_clear_func_t로 래핑합니다.
  • .accumulate<&fn>()void(State&, TypedArgs...)vef_vdf_accumulate_func_t로 래핑합니다. TypedArgs는 함수 시그니처에서 추론됩니다(IntArg, StringArg 등).
  • ResultWrapper 타입(IntResult, RealResult 등)은 결과 함수 시그니처에서 추론됩니다.
NULL을 반환하지 않는 카운터의 경우, 단순한 상태 타입을 사용합니다:

문장별 상태 (Prerun 및 Postrun)

일부 VDF는 단일 쿼리가 처리하는 모든 행을 통해 상태를 유지해야 합니다 — 호출 카운터, 캐시된 결과, 열린 리소스. Prerun 훅에서 할당하고, VDF 본문에서 액세스하고, Postrun 훅에서 해제합니다. 두 훅은 문장당 한 번 실행되며, VDF 본문은 행당 한 번 실행됩니다. .prerun<&Hook>().postrun<&Hook>()으로 등록합니다. 필수 시그니처는 다음과 같습니다: 원시 ABI 시그니처는 컴파일 타임에 거부됩니다. 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 타입을 노출합니다. PrerunArgTypeis_int(), is_real(), is_str(), is_custom() 조건식는 열 타입을 반영합니다. 이는 prerun에서 매개변수 타입을 검증하거나 PrerunResult::request_buffer_size(n)을 호출하여 결과 버퍼 크기를 지정하는 데 사용됩니다.

가변 인자 VDF

가변 인자 VDF는 SQL 타입의 임의 수의 인자를 수용합니다. .varargs()를 사용하여 선언하며, 이는 .no_params().param(TYPE)과 상호 배타적입니다. 본문은 일반적인 고정 인자 래퍼 대신 vsql::VarArgs 매개변수를 받습니다.
가변 인자 등록은 VEF 프로토콜 3을 필요로 합니다. 이전 서버는 설치 시 확장을 거부합니다.
프레임워크는 가변 인자 VDF의 인자 개수 및 타입을 검증할 수 없습니다. 가변 인자 등록과 함께 prerun 훅을 사용하여 잘못된 입력에 PrerunResult::error()를 호출하거나 결과 버퍼 크기를 지정하기 위해 PrerunResult::request_buffer_size(n)을 호출해야 합니다. 범위-기반 for 루프로 인자를 반복합니다. 각 AnyArg 요소는 값 읽기 전에 타입 검사를 요구합니다: 액세서 사용 전에 is_null()을 확인해야 합니다 — 모든 네 가지는 NULL 인자에서 정의되지 않습니다.

VEF_GENERATE_REGISTRATION

VEF_GENERATE_REGISTRATION은 내부 _vef_do_register() 헬퍼를 생성하여 확장 등록을 수행하지만, extern "C" 엔트리 포인트를 정의하지 않습니다. 테스트 빌드에서 등록 후 디스크립터를 수정해야 할 때 사용합니다. 일반 확장의 경우 대신 VEF_GENERATE_ENTRY_POINTS를 사용합니다.

타입 연산 빌더

확장이 사용자 정의 컬럼 타입을 정의하는 경우에만 필요합니다. 함수만 작성하는 경우 회귀 테스트 실행으로 건너뜁니다. 사용자 정의 타입은 엔진이 내부적으로 호출하는 세 가지 연산이 필요합니다: 인코딩(문자열 → 이진), 디코딩(이진 → 문자열), 비교. 해시는 선택적입니다. 이 연산들을 다음 C++ 시그니처에 맞게 구현하세요(<villagesql/vsql.h>를 통해 사용 가능합니다):

고정 길이 타입

이 연산을 vsql::make_type<kTypeName>()으로 등록합니다. 타입 이름은 비타입 템플릿 매개변수(NTTP)로 전달됩니다 — static constexpr const char[] 배열입니다. 빌더는 NTTP에서 TYPE::method 형식의 VDF 이름을 자동 생성하므로 수동 문자열 매칭이 필요하지 않습니다. 빌드된 타입 객체를 확장 빌더의 .type()에 전달하고, 타입 연산을 위한 별도의 .func() 호출은 필요하지 않습니다.
타입 이름은 static constexpr const char[] 변수여야 합니다 — 문자열 리터럴은 비타입 템플릿 매개변수로 사용할 수 없습니다. "MYTYPE"을 직접 전달하면 컴파일러 오류가 발생합니다:
아래와 같이 이름을 명명된 배열로 선언하세요.
build()from_string, to_string, 또는 compare가 누락된 경우 컴파일 타임에 실패합니다. 각 템플릿 메서드는 static_assert를 통해 함수 포인터 시그니처를 검사합니다.

내장 기본값

NOT NULL 사용자 정의 타입 컬럼이 IGNORE 모드(예: INSERT IGNORE 또는 UPDATE IGNORE)에서 NULL을 받을 때, 서버는 오류를 발생시키지 않고 내장 기본값을 호출하여 대체 값을 생성합니다. 내장 기본값은 문자열 표현을 제공하며, 서버는 타입의 from_string 함수를 사용하여 이진으로 변환합니다.
.intrinsic_default_str().intrinsic_default_vdf()를 모두 생략하면, 서버는 from_string("")을 대체로 호출합니다. 이는 타입이 처음 사용될 때(테이블 생성 시) 발생하며, INSTALL EXTENSION 시가 아닙니다. 인코딩 함수가 빈 문자열을 거부하거나 잘못된 바이트 수로 인코딩하는 경우, 타입 초기화가 오류로 실패하여 SQL 클라이언트에서 오류가 표시됩니다:
고정 길이 타입의 경우, 기본 문자열은 정확히 persisted_length 바이트로 인코딩되어야 합니다. 빈 문자열이 유효한 입력이 아닌 타입의 경우 명시적 기본값을 설정하세요.
문자열 리터럴: .intrinsic_default_str() 상수 기본값을 전달하려면 타입 빌더에 직접 문자열을 전달합니다(고정 길이 예제에서 보인 대로 .intrinsic_default_str("0")). VDF 기반: .intrinsic_default_vdf() + make_intrinsic_default 기본값이 타입 매개변수에 따라 달라질 때, 다음 시그니처 중 하나에 따라 함수를 구현합니다(사용 가능: <villagesql/vsql.h>):
중요 변경: IntrinsicDefaultFuncIntrinsicDefaultWithParamsFuncconst char* 대신 std::string을 반환합니다. 기존 내장 기본값 구현을 std::string 직접 반환하도록 업데이트하세요.
std::string 형식의 기본값 표현을 반환합니다. 오류 시 error_msg에 메시지를 쓰고 임의 값(SDK는 error_msg[0] != '\0'을 확인하여 오류 감지)을 반환합니다. make_intrinsic_default<&fn>("vdf_name")(VDF 이름을 한 개의 인자로)으로 등록하고, 타입 빌더에서 .intrinsic_default_vdf()로 참조합니다. 매개변수화된 타입 예제는 전체 등록 패턴을 보여줍니다.

매개변수화된 타입

변동 길이 타입은 인코딩, 디코딩, 비교, 해시 시 열의 선언된 매개변수를 필요로 하여 할당 크기 및 레이아웃을 결정합니다. 매개변수 구조체를 정의하고, 구문 분석 함수와 역 to_strings 함수를 구현합니다. 타입 빌더에 .params<P, &ParseFunc, &ToStringsFunc>()로 등록하고, 타입 연산 함수의 첫 번째 인자로 const P&를 사용합니다. SDK는 고유한 매개변수 조합당 한 번만 구문 분석을 실행하도록 캐시합니다. to_strings 함수는 parse의 역함수로, 타입 P를 표준 키/값 문자열 형식으로 쓰여서 서버가 parse가 소비하는 형식과 동일한 구조로 추론된 매개변수를 게시합니다.
타입 빌더에 .params<>()를 등록합니다. MYTYPE(N) 정수 구문을 처리하려면 .int_to_params<&mytype_int_to_params_fn>()을 사용하고, 매개변수를 검증하고 저장 크기를 계산하려면 .resolve_params<&mytype_resolve_params_fn>()을 사용합니다. 모든 유효한 매개변수화에서의 최대 persisted_length를 지정하기 위해 .max_persisted_length(N)을 호출합니다. 서버는 이 값을 타입 매개변수 추론 경로에서만 사용하며, 아직 매개변수를 추론하지 않았으므로 resolve_params를 참조하여 인코딩 버퍼 크기를 결정할 수 없습니다. VDF 기반 내장 기본값의 경우, .intrinsic_default_vdf()와 VDF 이름을 사용하고, make_intrinsic_default<&mytype_default>()으로 별도로 VDF를 등록합니다.
매개변수화된 변형 — TypeEncodeWithParamsFunc<P>, TypeDecodeWithParamsFunc<P>, TypeCompareWithParamsFunc<P>, TypeHashWithParamsFunc<P>ParamsToStringsFunc<P>(void fn(const P&, std::map<std::string,std::string>&))은 <villagesql/vsql.h>를 통해 사용 가능합니다. vsql::make_type 템플릿 메서드는 매개변수 인자를 감지하고 자동으로 매개변수 캐시를 통해 라우팅합니다. 인코딩 함수는 vsql::MaybeParams<P> &를 첫 번째 인자로 받습니다. 런타임에서 is_known()은 항상 참이며, value()const P&를 반환합니다. 디코딩, 비교, 해시 변형은 vsql::CustomArgWith<P>를 받으며, params() 액세서는 const P&를 반환합니다.

저장 프로시저에서의 사용자 정의 타입

사용자 정의 확장 타입은 저장 프로시저 매개변수 타입 및 DECLARE 변수 선언에서 사용할 수 있습니다. 서버는 실행 시 설치된 확장의 타입 메타데이터를 사용하여 사용자 정의 타입을 해결합니다.

확장 시스템 변수

확장 시스템 변수는 미리보기 기능입니다 — 전체 API 참조, 팩토리 함수, SQL 액세스 및 완전한 예제는 미리보기 기능을 참조하세요.

확장 상태 변수

확장 상태 변수는 미리보기 기능입니다 — 전체 API 참조, 팩토리 함수, SQL 액세스 및 완전한 예제는 미리보기 기능을 참조하세요.

키링크 액세스

키링크 액세스는 미리보기 기능입니다 — 전체 API 참조, 결과 코드 및 완전한 예제는 미리보기 기능을 참조하세요.

컬럼 저장

컬럼 저장은 미리보기 기능입니다 — 전체 API 참조 및 완전한 예제는 미리보기 기능을 참조하세요.

확장 등록 메타데이터 검사

INFORMATION_SCHEMA.EXTENSION_REGISTRATION은 로드된 확장의 모든 VEF 등록 구조체를 JSON 문서로 노출합니다. INSTALL EXTENSION 후 서버가 확장의 함수, 타입, 시스템 변수를 올바르게 해석했는지 확인하는 데 사용합니다.

회귀 테스트 실행

VillageSQL 빌드 디렉터리에서 MySQL 테스트 러너를 사용하여 확장 회귀 테스트를 실행합니다.

전체 테스트 세트 실행

확장의 모든 테스트를 실행하려면:

개별 테스트 실행

단일 테스트 케이스를 실행하려면, 스위트 경로와 테스트 이름을 지정합니다:

새로운 테스트 만들기

새 기능 추가 또는 버그 수정 시 대응되는 회귀 테스트를 추가해야 합니다.

테스트 위치

확장 테스트는 VillageSQL 서버의 mysql-test/suite/ 트리가 아닌, 확장 자체 리포지토리의 test/ 디렉터리에 위치합니다.
  • 테스트 파일은 .test로 끝나며 test/t/에 배치됩니다.
  • 예상 결과 파일은 .result로 끝나며 test/r/에 배치됩니다.
예를 들어, my_extension이라는 확장의 경우:
  • test/t/my_new_test.test
  • test/r/my_new_test.result

테스트 파일 규칙

일반적인 확장 테스트는 확장 설치, SQL 실행, 제거를 포함합니다:
테스트 출력에 테스트 러너의 임시 디렉터리 경로가 포함된 경우, .test 파일 내에 이 지시어를 추가하여 경로를 정규화하세요. 그렇지 않으면 기록된 결과에 다른 머신에서 오류를 유발하는 절대 경로가 포함됩니다:

테스트 추가 단계

  1. .test 파일을 확장의 test/t/ 디렉터리에 생성합니다.
  2. .result 파일을 확장의 test/r/ 디렉터리에 생성합니다.
  3. --record로 테스트를 실행하여 예상 출력을 생성합니다:
  4. 생성된 .result 파일의 출력을 확인하여 예상과 일치하는지 확인합니다.

테스트 디버깅

테스트가 실패하면 테스트 프레임워크가 상세한 로그를 제공합니다.
  • 테스트 출력: mysql-test/var/log/mysqltest.log (통합) 또는 mysql-test/var/log/<test_name>/ (테스트별 디렉터리)를 확인합니다.
  • 서버 오류 로그: mysql-test/var/log/mysqld.1.err를 확인합니다. VillageSQL 전용 로그 메시지(LogVSQL()을 통해 출력됨)는 서버가 --log-error-verbosity=3 옵션으로 실행될 때만 표시됩니다.
  • 차이점: 프레임워크는 실제 출력과 예상된 .result 파일 간의 차이를 출력합니다.
테스트를 추가 디버그 정보와 함께 실행하려면:

참고 자료