Skip to main content
Este guia é a referência aprofundada para escrever implementações de VDF em C++. Ele é o complemento de Criando Extensões em C++, que cobre as etapas de compilação de ponta a ponta, e de Testes em C++, que cobre o ciclo de teste e iteração.
O Protocol 3 do VEF é estável desde a v0.0.4. O Protocol 4 está em desenvolvimento e está disponível apenas por meio de cabeçalhos de ABI de desenvolvimento opcionais (-DVSQL_USE_DEV_ABI=ON). Extensões compiladas com o antigo Protocol 2 são rejeitadas pelo servidor e precisam ser recompiladas.

Escrevendo Funções de Extensão

As funções de extensão são escritas em C++ e registradas com o VEF. Inclua um único cabeçalho para acessar as APIs de VDF, de tipo e do construtor de extensão:

Segurança de Exceções

Conter as exceções é responsabilidade da extensão. O SDK não envolve a sua implementação em um try/catch, e o servidor também não, então uma exceção que escapa de um ponto de entrada desenrola a pilha através da fronteira da ABI C sem nenhum tratador acima dela e o processo do servidor é encerrado. Uma entrada SQL comum chega a isso: std::stoi() lança uma exceção em uma string não numérica, e nlohmann::json::dump() lança uma exceção em um texto que não é UTF-8 válido. Envolva o corpo de cada ponto de entrada que você registrar: VDFs, funções de agregação, ganchos prerun e postrun, e as operações de um tipo personalizado.
Capture ... além de const std::exception &: uma dependência pode lançar um tipo que não deriva de std::exception, e esse ainda encerra o servidor. Escolha entre warning() e error() com o mesmo critério de qualquer outra falha — warning() para entrada inválida à qual a instrução deve sobreviver, error() para os casos em que continuar é inseguro.

Tipos de Argumento e Resultado

Os parâmetros e resultados de VDF são passados como tipos de argumento e resultado com segurança de tipo. O framework os detecta a partir da assinatura da sua função e se adapta automaticamente — a sintaxe de registro make_func permanece inalterada. Tipos de argumento: IntArg, RealArg, StringArg, CustomArg — cada um fornece is_null() e value(). Para tipos personalizados parametrizados, CustomArgWith<P> adiciona um acessador params() que retorna a struct de params analisada e armazenada em cache (consulte Tipos Parametrizados). Tipos de resultado: IntResult, RealResult, StringResult, CustomResult — cada um fornece set_null(), warning(msg) e error(msg). Os resultados escalares também fornecem set(value). Os resultados de buffer fornecem buffer() e set_length(len). StringResult também fornece set(std::string_view), que copia até buffer().size() bytes da view e define o comprimento em uma única chamada. Para tipos personalizados parametrizados, CustomResultWith<P> adiciona um acessador params(). Tipo Span: value() e buffer() nos tipos de argumento e resultado orientados a bytes retornam um vsql::Span<T> — uma view não proprietária sobre uma sequência contígua de T com data(), size(), empty(), begin()/end() e operator[]. No C++20 ele é um alias para std::span<T>; no C++17, este SDK fornece uma implementação mínima compatível, de modo que o mesmo código compila em qualquer um dos padrões. Ele está disponível através de <villagesql/vsql.h>. warning(msg) retorna SQL NULL para a linha e anexa um aviso SQL. Esse NULL é um valor comum do ponto de vista do servidor, portanto só se torna um erro de instrução da mesma forma que qualquer NULL: gravá-lo em uma coluna NOT NULL falha sob STRICT_TRANS_TABLES, assim como INSERT INTO t VALUES (NULL) falharia. Gravá-lo em uma coluna que aceita NULL tem sucesso, com o aviso incluído. Use warning(msg) para entrada inválida recuperável, como uma string que não pode ser analisada em uma função de codificação. Use error(msg) para dados armazenados corrompidos ou qualquer condição em que continuar seja inseguro. A mensagem de ambos é truncada para caber no buffer de erro interno do servidor, se necessário. Exemplo escalar — soma dois inteiros:
Exemplo binário — transforma um valor de tipo personalizado no buffer de resultado:
Para StringResult e CustomResult, escreva em buffer() e, em seguida, chame set_length() com o número de bytes escritos. buffer().size() é a capacidade máxima. Para VDFs que retornam um tipo personalizado (.returns(MYTYPE)), o servidor dimensiona o buffer de resultado para o persisted_length do tipo de retorno resolvido automaticamente — os autores de extensões não precisam declarar .buffer_size(...) no construtor da função nesse caso. Se prerun aumentar ainda mais o buffer, esse tamanho maior é preservado. É isso que permite, por exemplo, que SVECTOR::from_string('[…1024 floats…]') codifique um vetor amplo sem que o buffer de resultado fique sem espaço. Você pode usar estilos diferentes entre funções na mesma extensão — o estilo de cada função é determinado por sua própria assinatura.

VDFs Agregadas

As VDFs agregadas acumulam estado entre linhas dentro de cada grupo GROUP BY e retornam um único resultado por grupo, como SUM ou COUNT do SQL. Use make_aggregate_func<State, &result_fn>("name") para registrar uma. O tipo State é o buffer de acumulação por grupo; prerun e postrun são gerados automaticamente para alocá-lo e removê-lo. A função de resultado deve ter a assinatura void(const State&, ResultType) onde ResultType é um dentre IntResult, RealResult, StringResult, CustomResult ou CustomResultWith<P>. Chame out.set(value) para retornar um valor ou out.set_null() para retornar SQL NULL. Tanto .clear<>() quanto .accumulate<>() são obrigatórios. O construtor impõe isso em tempo de compilação (via build()), e o servidor valida novamente no momento do INSTALL EXTENSION — clear redefine o estado, accumulate acumula as linhas e a função de resultado lê o estado final.
Como funcionam os métodos do construtor:
  • make_aggregate_func<State, &result_fn>() gera automaticamente prerun e postrun (inicializa por valor e remove o State).
  • .clear<&fn>() registra sua função de redefinição void(State&).
  • .accumulate<&fn>() registra sua função de acumulação void(State&, TypedArgs...). Os TypedArgs são deduzidos da assinatura da função (IntArg, StringArg, etc.). Chame .accumulate<>() depois de todas as chamadas .param(TYPE): o construtor verifica a contagem de argumentos tipados contra a contagem de parâmetros SQL declarados em tempo de compilação, então chamá-lo antes falha na compilação.
  • O tipo de resultado (IntResult, RealResult, etc.) é deduzido da assinatura da função de resultado.
Para um contador que nunca retorna NULL, use um tipo de estado simples:
Uma VDF agregada com StringResult retorna texto: o resultado informa o charset e a collation utf8mb4_bin, de modo que os clientes o exibem como caracteres em vez de hexadecimal — o mesmo que o caminho STRING da VDF escalar. Ela também respeita .max_result_length(n) — o que exige os cabeçalhos de ABI de desenvolvimento opcionais (-DVSQL_USE_DEV_ABI=ON) e o Protocol 4 — da mesma forma, dimensionando um resultado agregado materializado (uma tabela temporária de GROUP BY/DISTINCT, CREATE TABLE ... SELECT ou UNION) de modo que ele não seja truncado na largura do argumento. Consulte Tamanhos de Buffer Personalizados para as regras de dimensionamento e o limite.

Estado por Instrução (Prerun e Postrun)

Algumas VDFs precisam de estado que abranja cada linha que uma única consulta toca — um contador de chamadas, um resultado em cache, um recurso aberto. Aloque-o em um gancho prerun, acesse-o a partir do corpo da VDF e libere-o em um gancho postrun. Ambos os ganchos são executados uma vez por instrução; o corpo da VDF é executado uma vez por linha. Registre-os com .prerun<&Hook>() e .postrun<&Hook>(). As assinaturas obrigatórias são: Use PrerunResult::set_user_data(void*) para armazenar o estado; use PostrunArgs::delete_state<T>() para liberá-lo. Se prerun chamar set_user_data(new T{}), postrun deve chamar delete_state<T>() — o SDK não libera automaticamente. PrerunArgs::type_at(i) expõe o tipo SQL declarado de cada argumento antes que qualquer linha seja lida; os predicados is_int(), is_real(), is_str(), is_custom() no PrerunArgType retornado espelham os tipos de coluna. Use isto em prerun para validar os tipos de argumento ou chame PrerunResult::request_buffer_size(n) para dimensionar o buffer de resultado.

VDFs com Varargs

Uma VDF com varargs aceita qualquer número de argumentos de qualquer tipo SQL. Declare uma com .varargs() no construtor da função, o que é mutuamente exclusivo com .no_params() e .param(TYPE). O corpo recebe um argumento vsql::VarArgs em vez dos tipos de argumento usuais de aridade fixa.
O registro de varargs requer o Protocol 3 do VEF. Servidores mais antigos rejeitam a extensão no momento da instalação.
O framework não pode validar a contagem ou os tipos de argumento para VDFs com varargs. Combine cada registro de varargs com um gancho prerun que chame PrerunResult::error() em entrada inválida ou PrerunResult::request_buffer_size(n) para dimensionar o buffer de resultado. Itere sobre os argumentos com range-for. Cada elemento AnyArg requer uma verificação de tipo antes de ler seu valor: Verifique is_null() antes de qualquer acessador — todos os quatro são indefinidos em um argumento nulo. PrerunArgType::custom_type() retorna o nome do tipo personalizado do argumento como um std::string_view — vazio para argumentos que não são de tipo personalizado. is_custom() sozinho aceita todo tipo personalizado registrado no servidor; combiná-lo com uma comparação de custom_type() restringe uma chamada varargs a um tipo específico:

Decidindo os Parâmetros de Tipo de uma Função no Momento da Resolução

As regras internas do servidor normalmente determinam os parâmetros de um tipo de retorno a partir dos tipos declarados dos seus argumentos. Às vezes elas não conseguem. Por exemplo, tvector_concat(TVECTOR(M), TVECTOR(N)) retorna um TVECTOR(M+N). As regras internas rejeitam dois argumentos TVECTOR cujas dimensões diferem, e elas podem copiar os parâmetros de um argumento para o tipo de retorno, mas nunca somá-los. Para casos como este, registre um gancho bind-and-check-types com .bind_and_check_types<&Hook>() no construtor da função, com a assinatura:
.bind_and_check_types() e .varargs() são mutuamente exclusivos na mesma função — uma VDF com varargs não tem uma lista fixa de argumentos sobre a qual o gancho possa raciocinar. Declarar .varargs() antes de .bind_and_check_types<&Hook>() na cadeia do construtor é uma falha de static_assert em tempo de compilação. Declará-los na outra ordem compila, mas a extensão é rejeitada quando o servidor a carrega: o registro percorre o estado final construído de cada função e rejeita a primeira que é varargs e tem um gancho de bind ao mesmo tempo, com:
.bind_and_check_types<&Hook>() e <villagesql/vsql/bind_check_types.h> são declarados apenas nos cabeçalhos de ABI de desenvolvimento opcionais (-DVSQL_USE_DEV_ABI=ON) e exigem o Protocol 4 do VEF. Uma compilação padrão com a ABI estável não tem nenhum dos dois, então registrar um gancho falha na compilação.
O servidor chama o gancho entre duas passagens que ele faz sobre os argumentos de uma função: a primeira passagem verifica o tipo base de cada argumento, o gancho é executado em seguida, e a segunda passagem aplica a resposta do gancho para cada argumento em que ele deu uma. Com um gancho instalado, a primeira passagem não coleta parâmetros compartilhados entre argumentos do mesmo tipo personalizado, então nenhum deles precisa concordar com os outros. Um argumento cujo próprio tipo ainda não está resolvido, como um literal de string simples, recebe seus parâmetros apenas do gancho. Os parâmetros de um tipo de retorno parametrizado também vêm do gancho, no lugar da regra interna que os copia de um argumento. A extensão de exemplo vsql_tvector na árvore de código-fonte do servidor registra um gancho desses em tvector_concat:
O gancho permite que dois vetores de dimensões diferentes sejam combinados, e ele conta os elementos de um literal constante. Uma compilação do servidor a partir do código-fonte inclui o vsql_tvector; os tarballs de release não.

Ganchos de Carregamento e Descarregamento da Extensão

Às vezes uma extensão tem trabalho a fazer uma vez por carregamento, em vez de uma vez por chamada — escolher uma implementação específica de CPU, ler a sua própria configuração uma única vez ou construir uma tabela de consulta que as VDFs depois leem. Registre esse trabalho com .on_init<&Fn>() no construtor da extensão, e a desmontagem correspondente com .on_deinit<&Fn>(). Ambos recebem uma função com a assinatura void(), e precisam ser registrados juntos — declarar um sem o outro falha na compilação. Uma extensão que chega ao servidor com um par incompleto por algum outro caminho é rejeitada no carregamento:
.on_init<&Fn>() e .on_deinit<&Fn>() são declarados apenas nos cabeçalhos de ABI de desenvolvimento opcionais (-DVSQL_USE_DEV_ABI=ON). Uma compilação padrão com a ABI estável não tem nenhum dos dois métodos, então registrar um gancho falha na compilação.
on_init é executado uma vez por carregamento — no INSTALL EXTENSION, e novamente a cada inicialização do servidor — depois que o servidor validou a extensão e populou as capabilities exigidas por ela, de modo que uma variável de sistema registrada já contém o seu valor configurado. Na inicialização, especificamente, on_init é adiado até que os valores persistidos das variáveis de sistema tenham sido aplicados, então ele nunca vê um padrão compilado no lugar de um valor definido com SET PERSIST. Ele nunca é executado para uma extensão que o servidor rejeitou. on_deinit é executado no descarregamento — UNINSTALL EXTENSION ou desligamento do servidor — antes que o servidor despopule essas mesmas capabilities, então ele vê o mesmo estado ativo que on_init viu.
Depois de INSTALL EXTENSION my_extension, SELECT fast_sqrt(16) retorna 4 — um resultado que só é alcançável se build_table já tiver sido executado.

VEF_GENERATE_REGISTRATION

VEF_GENERATE_REGISTRATION cria um auxiliar interno _vef_do_register() que realiza o registro da extensão, mas não define os pontos de entrada extern "C". Use-o quando precisar personalizar o comportamento de vef_register — por exemplo, para corrigir descritores após o registro em uma compilação de teste. Para extensões normais, use VEF_GENERATE_ENTRY_POINTS em vez disso.

Operações de Tipos Personalizados

Para a referência completa do construtor de operações de tipo — codificação, decodificação, comparação, hash, padrões intrínsecos e tipos parametrizados — consulte Operações de Tipo.

Capabilities Preview

As seguintes capabilities do VEF estão disponíveis como cabeçalhos Preview opcionais. A ABI e a API ainda estão em desenvolvimento ativo; consulte Capabilities Preview para a referência completa.

Inspecionando os Metadados de Registro da Extensão

INFORMATION_SCHEMA.EXTENSION_REGISTRATION expõe a struct de registro do VEF em memória para cada extensão carregada como um documento JSON. Use-o para verificar se o servidor analisou corretamente as funções e os tipos da sua extensão após o INSTALL EXTENSION.

Veja Também