Prefácio
A partir do Firebird 3.0, você pode escrever várias extensões (plugins) para o Firebird SQL. A extensão mais simples é um procedimento/função externa (UDR) (veja o artigo Writing UDF in Pascal). UDR não é um plugin do Firebird no verdadeiro sentido da palavra, mas permite expandir significativamente as capacidades do SGBD. No entanto, a engine do Firebird tem mais capacidades para estender a funcionalidade com vários plugins.
O Firebird possui os seguintes tipos de plugins (constantes da classe IPluginManager
):
TYPE_PROVIDER
— providers;
TYPE_AUTH_SERVER
— autenticação do lado do servidor;
TYPE_AUTH_CLIENT
— autenticação do lado do cliente;
TYPE_AUTH_USER_MANAGEMENT
— gerenciamento de usuários;
TYPE_EXTERNAL_ENGINE
— engines externas. Elas são intermediárias entre o código de stored procedures/funções/triggers externos escritos em qualquer linguagem de programação e o Firebird. Exemplos de tais plugins são udr_engine
para interação com UDRs escritas em C++/Delphi ou libfbjava
para UDRs em Java;
TYPE_TRACE
— plugin de rastreamento;
TYPE_WIRE_CRYPT
— criptografia de tráfego de rede;
TYPE_DB_CRYPT
— criptografia de banco de dados;
TYPE_KEY_HOLDER
— detentor de chave para o plugin de criptografia de banco de dados;
TYPE_REPLICATOR
— plugin replicador.
Neste artigo, veremos talvez o tipo de plugin mais complexo e interessante — os providers.
1. Como tudo começou?
Em 2023, comecei a trabalhar no desenvolvimento de um plugin para o HQbird para acessar bancos de dados de outros SGBDs (não Firebird) via EXECUTE STATEMENT ON EXTERNAL DATA SOURCE
(doravante EDS). Vlad Khorsun, desenvolvedor principal do Firebird, foi o consultor deste projeto.
Atualmente, no HQbird 2024, dois plugins estão disponíveis para Firebird 4.0 e 5.0: MySQLEngine
e ODBCEngine
. O MySQLEngine
foi criado primeiro e foi um cavalo de testes, os esforços principais foram focados no ODBCEngine
.
2. Como a funcionalidade do EDS pode ser estendida?
Existem duas opções:
- Estender o mecanismo EDS no núcleo do Firebird;
- Implementar um dos plugins do Firebird.
Serei direto com você - não importa qual caminho você escolha, você precisará mergulhar no código-fonte do Firebird. Simplesmente não há como contornar isso, porque você não encontrará essa informação documentada em nenhum outro lugar.
Primeiro, vamos considerar a primeira opção.
3. Extensão do mecanismo EDS
A implementação do EDS está localizada no diretório /src/jrd/extds/
. O próprio mecanismo EXECUTE STATEMENT
pode ser estendido a partir do Firebird 2.5. Para este propósito, os chamados EDS Providers foram inventados. Existem dois EDS providers no Firebird:
- Provider Firebird.
EXECUTE STATEMENT
é usado para acessar bancos de dados Firebird externos.
- Provider Internal.
EXECUTE STATEMENT
é usado para acessar o banco de dados atual.
O código fornece a capacidade de adicionar novos providers (veja /src/jrd/extds/ExtDS.cpp
).
void Manager::addProvider(Provider* provider)
{
// TODO: if\when usage of user providers will be implemented,
// need to check provider name for allowed chars (file system rules ?)
// ...
}
Embora esses providers estejam ocultos de você, é possível especificar explicitamente o provider a ser usado na string de conexão do EXECUTE STATEMENT ON EXTERNAL
.
SQL = 'SELECT MON$ATTACHMENT_NAME FROM MON$ATTACHMENTS';
FOR
EXECUTE STATEMENT :SQL
ON EXTERNAL 'Firebird::inet://localhost/ext_db'
AS USER 'SYSDBA' PASSWORD 'masterkey'
INTO NAME
DO SUSPEND;
-- returns ext_db
FOR
EXECUTE STATEMENT :SQL
ON EXTERNAL 'Internal::inet://localhost/ext_db'
AS USER 'SYSDBA' PASSWORD 'masterkey'
INTO NAME
DO SUSPEND;
-- returns self_db (connection string is ignored)
Então, se planejamos estender o mecanismo do provider EDS ODBC, o EXECUTE STATEMENT ON EXTERNAL
ficaria assim:
FOR
EXECUTE STATEMENT :SQL
ON EXTERNAL 'ODBC::<conn_string>'
AS USER 'user' PASSWORD 'secret'
INTO NAME
DO SUSPEND;
Isso é possível, mas existem desvantagens significativas nesta abordagem:
- Os dados do EDS Provider não são implementados como plugins (bibliotecas dinâmicas), o que significa que a modificação dos códigos-fonte da engine do Firebird é necessária.
- A solução é muito mais difícil de manter. A portabilidade manual da funcionalidade para novas versões é necessária, por exemplo, quando o Firebird 6.0 e versões posteriores forem lançados.
No entanto, tendo estudado as características do EXECUTE STATEMENT ON EXTERNAL
, pode-se notar um detalhe importante — o provider Firebird::
funciona através da API usual do Firebird, que é usada em aplicações, ou seja, o EDS atua como um cliente regular. E isso leva a outro pensamento: e se a API de uma fonte de dados externa for encapsulada na API usada para acessar o banco de dados Firebird?
4. Plugin do tipo Provider
Se você estudar cuidadosamente a documentação existente do Firebird, descobrirá que para esses propósitos, a partir do Firebird 3.0, um tipo especial de plugin chamado Provider (não confundir com EDS Provider) foi fornecido. Uma breve descrição do que é pode ser encontrada em doc/README.providers.html
.
Um plugin do tipo Provider fornece uma única API para interagir com o Firebird. Não importa se a interação física ocorre pela rede ou diretamente com a engine do Firebird.
Onde você encontra providers? Abra o firebird.conf
e você verá a seguinte linha lá:
Providers = Remote, Engine13, Loopback
O provider Remote
é responsável pela interação pela rede, e o Engine13
é o núcleo para interação com ODS13, usado diretamente ao trabalhar em modo embarcado. Os providers são tentados sequencialmente, e aquele que não se recusa a trabalhar é o ativo.
Talvez o exemplo mais conhecido que demonstra o trabalho dos providers seja a configuração:
Providers = Remote,Engine13,Engine12,Loopback
Esta configuração permite que o Firebird 5.0 funcione tanto com bancos de dados nativos ODS 13.1 quanto com bancos de dados ODS 12.0 (Firebird 3.0).
Se fornecermos uma API do Firebird para trabalhar com MySQL ou ODBC com a ajuda de nosso provider, a configuração pode ser a seguinte:
Providers = Remote,Engine13,ODBCEngine,Loopback
Providers = ODBCEngine,Remote,Engine13,Loopback
Providers = MySQLEngine,Remote,Engine13,Loopback
5. Implementação do provider
Agora vamos falar diretamente sobre a implementação de nossos próprios providers. Quais interfaces de API precisam ser implementadas?
IProvider
IAttachment
ITransaction
IStatement
IResultSet
IBlob
Por favor, note que nem todos os métodos dessas interfaces precisam ser implementados. Implementaremos apenas aqueles que são necessários para que o EXECUTE STATEMENT ON EXTERNAL
funcione, e faremos stubs para o resto.
Além do próprio provider, sua factory deve ser implementada. Você pode dar uma olhada na implementação da factory e no registro do provider usando o exemplo dos providers EngineXX em /src/jrd/jrd.cpp
(.h
).
namespace {
template <class P>
class Factory : public IPluginFactoryImpl<Factory<P>, CheckStatusWrapper>
{
public:
// IPluginFactory implementation
IPluginBase* createPlugin(CheckStatusWrapper* status, IPluginConfig* factoryParameter)
{
try {
IPluginBase* p = new P(factoryParameter);
p->addRef();
return p;
}
catch (const std::exception& e) {
Firebird::setStatusError(status, e.what());
}
return nullptr;
}
};
static Factory<OdbcEngine::OdbcProvider> engineFactory;
} // namespace
void registerEngine(IPluginManager* iPlugin)
{
UnloadDetectorHelper* module = getUnloadDetector();
module->setCleanup(shutdownBeforeUnload);
module->setThreadDetach(threadDetach);
iPlugin->registerPluginFactory(IPluginManager::TYPE_PROVIDER, ODBC_ENGINE_NAME, &engineFactory);
module->registerMe();
}
extern "C" FB_DLL_EXPORT void FB_PLUGIN_ENTRY_POINT(IMaster * master)
{
CachedMasterInterface::set(master);
registerEngine(PluginManagerInterfacePtr());
}
5.1. Implementação da interface IProvider
O que precisa ser implementado na interface IProvider
(ODBCProvider
, MySQLProvider
)?
A interface IProvider
tem as seguintes funções:
IAttachment* attachDatabase(...)
(obrigatório)
IAttachment* createDatabase(...)
(lançar erro isc_unavailable
)
IService* attachServiceManager(...)
(lançar erro isc_unavailable
)
void shutdown(...)
(não necessário)
void setDbCryptCallback(...)
(não necessário)
Nesta interface, o principal é implementar o método IProvider::attachDatabase
. Ele deve fazer o seguinte:
- Analisar a string de conexão e extrair o prefixo nela
- Se o prefixo corresponder ao nosso prefixo, criar uma conexão com o BD, caso contrário, lançar o erro
isc_unavailable
.
- O prefixo é necessário para determinar rapidamente se deve ser feita uma tentativa de conexão, que pode não ser barata, ou deixar o próximo provider tentar se conectar ao BD.
- Os seguintes prefixos são fornecidos:
- Para ODBC, é:
:odbc:
ou odbc://
- Para MySQL, é:
:mysql:
ou mysql://
Aqui está um pequeno fragmento desta função:
IAttachment* ODBCProvider::attachDatabase(CheckStatusWrapper* status, const char* fileName,
unsigned dpbLength, const unsigned char* dpb)
{
debug_print_call();
status->clearException();
std::string dbFileName(fileName);
auto poviderPos = dbFileName.find(":odbc:");
std::string connStr;
if (poviderPos == 0) {
connStr = dbFileName.substr(6);
}
else if (poviderPos = dbFileName.find("odbc://"); poviderPos == 0) {
connStr = dbFileName.substr(7);
}
else {
// It is important to set the error with the status isc_unavailable
// to pass control to the next provider
const ISC_STATUS statusVector[] = {
isc_arg_gds, isc_unavailable,
isc_arg_end
};
status->setErrors(statusVector);
return nullptr;
}
....
}
As funções IProvider::createDatabase
e IProvider::attachServiceManager
não são necessárias para o funcionamento do EDS, mas ainda precisam ser implementadas e o erro isc_unavailable
lançado nelas. Isso é necessário para que o trabalho das cadeias de providers não seja interrompido.
5.2. Implementação da interface IAttachment
Os seguintes métodos devem ser implementados na interface IAttachment
(MySQLAttachment
, ODBCAttachment
):
void getInfo(status, ...)
ITransaction* startTransaction(status, ...)
IBlob* createBlob(status, ...)
IBlob* openBlob(status, ...)
IStatement* prepare(status, ...)
ITransaction* execute(status, ...)
IResultSet* openCursor(status, ...)
— este método nunca é chamado no EDS, pois ele sempre executa apenas statements preparados
void detach(status)
void dropDatabase(status)
— em teoria, este método não é necessário, mas às vezes o fluxo de controle entra nele, então simplesmente chamamos IProvider::detach
nele.
5.2.1. Implementação de IAttachment::getInfo
Neste método, você precisa retornar uma resposta para solicitações com as tags isc_info_db_sql_dialect
e fb_info_features
. A tag fb_info_features
é usada para retornar a funcionalidade suportada do seu provider. Os valores possíveis são descritos pela seguinte enumeração:
enum info_features // response to fb_info_features
{
fb_feature_multi_statements = 1, // Multiple prepared statements in single attachment
fb_feature_multi_transactions= 2, // Multiple concurrent transaction in single attachment
fb_feature_named_parameters= 3, // Query parameters can be named
fb_feature_session_reset= 4, // ALTER SESSION RESET is supported
fb_feature_read_consistency= 5, // Read consistency TIL is supported
fb_feature_statement_timeout= 6, // Statement timeout is supported
fb_feature_statement_long_life = 7, // Prepared statements are not dropped on transaction end
fb_feature_max // Not really a feature. Keep this last.
};
5.2.2. Implementação de IAttachment::startTransacxconteudoxconteudo