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)를 사용합니다. 두 메시지는 서버 내부 오류 버퍼에 맞게 필요한 경우 자동으로 잘립니다.
스칼라 예제 — 두 정수를 더합니다:
StringResult 및 CustomResult의 경우, 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 타입은 그룹별 누적 버퍼입니다. prerun 및 postrun은 자동으로 생성되어 할당 및 삭제합니다.
결과 함수는 void(const State&, ResultWrapper) 시그니처를 가져야 하며, ResultWrapper는 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을 자동 생성합니다(값 초기화 및 삭제)..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등)은 결과 함수 시그니처에서 추론됩니다.
문장별 상태 (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 타입을 노출합니다. PrerunArgType의 is_int(), is_real(), is_str(), is_custom() 조건식는 열 타입을 반영합니다. 이는 prerun에서 매개변수 타입을 검증하거나 PrerunResult::request_buffer_size(n)을 호출하여 결과 버퍼 크기를 지정하는 데 사용됩니다.
가변 인자 VDF
가변 인자 VDF는 SQL 타입의 임의 수의 인자를 수용합니다..varargs()를 사용하여 선언하며, 이는 .no_params() 및 .param(TYPE)과 상호 배타적입니다. 본문은 일반적인 고정 인자 래퍼 대신 vsql::VarArgs 매개변수를 받습니다.
프레임워크는 가변 인자 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() 호출은 필요하지 않습니다.
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>):
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.testtest/r/my_new_test.result
테스트 파일 규칙
일반적인 확장 테스트는 확장 설치, SQL 실행, 제거를 포함합니다:테스트 추가 단계
.test파일을 확장의test/t/디렉터리에 생성합니다.- 빈
.result파일을 확장의test/r/디렉터리에 생성합니다. --record로 테스트를 실행하여 예상 출력을 생성합니다:- 생성된
.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파일 간의 차이를 출력합니다.
참고 자료
- 확장 프로그램 생성 — 엔드투엔드 빌드 단계, CMake 설정 및 설치
- 확장 API 참조 — VDF 계약, null 처리 및 버퍼 크기 조정
- 확장 아키텍처 — 라이프사이클, Victionary 캐싱, 성능 패턴 및 보안 모델

