Skip to content
mobius1qwe edited this page Nov 9, 2026 · 5 revisions

Internos: DBWare 🚧

Home > Guia de contribuição > Internos: DBWare

Para quem mexe no código: módulo, pool de conexões, cache de SQL e os conectores FireDAC, Zeos e SQLdb. Aqui fica o porquê de cada parte ser como é, e o que não pode mudar sem quebrar algo. O código traz só a descrição curta de cada coisa.

Conteúdo da versão 2.0, ainda em desenvolvimento (branch 2.0).

Storage XML e o formato nativo

  • Quem decide se a resposta vai no formato nativo é o link do cliente. O TRALDBSQLCache chama PrepareForDataSet com o dataset que vai ler a resposta; o TRALStorageXMLLink anota se é FireDAC, Zeos ou TBufDataset (pelo nome da classe, para não depender dos pacotes deles) e manda isso nas propriedades do storage. No servidor, o TRALDBModule pergunta ao storage (AllowNative) se aquele par cliente/driver aceita o nativo. Os outros links respondem como antes: nativo só com o mesmo driver.
  • FireDAC grava e lê XML pelo MSXML, que é COM. A thread do servidor e um programa console não iniciaram COM, e o FireDAC falha com "No active document". RALDBFireDAC e RALDBFiredacMemTable chamam CoInitializeEx em volta do sfXML, e só desfazem o que eles fizeram.
  • O fcl-db só lê dfXML. dfXMLUTF8 existe no enum e não tem leitor registrado, nem no FPC 3.3. O texto do dfXML passa pela codepage do sistema, que o RAL deixa em UTF-8 (LazUTF8), então os acentos sobrevivem.
  • O nativo do sqldb abre a query com buffer. TBufDataset.SaveToStream não funciona numa query unidirecional, e o OpenNative do sqldb só serve à resposta nativa. O TRALDBBufDataset carrega a resposta nativa com as definições que vêm nela: as que ele adivinhou do getsqlfields saem antes, como no memtable do FireDAC.

O formato que viaja (RALStorageTypes)

Na 1.x estas funções ficam em RALDBTypes; na 2.0 a unit é RALStorageTypes, e RALDBTypes repassa para ela.

  • Dois valores viajam exatos. NUMERIC/DECIMAL (ftBCD, ftFMTBcd) vão como sftBCD, com os dígitos em texto: como double, 12345678901234.5678 chegaria 12345678901234.6 fora do caminho nativo do FireDAC. E os milissegundos das datas vão no storage BSON (o formato antigo, do kxBSON, gravava segundos inteiros; a leitura aceita os dois) e no tempo Unix (dtfUnix, com decimais do segundo). Um leitor antigo não lê nenhum dos dois: RALLegacyWire := True grava o formato antigo para um lado cujos pares são mais velhos. A leitura aceita os dois formatos.
  • O stream nativo carrega FieldType, não RALFieldType (TRALDBInfoField.NativeDriver). Um cliente desse driver que monta os campos pelo schema (o Fields Editor monta) tem de usar FieldType, senão o campo não bate com o que carrega: um NUMERIC que o FireDAC leva como ftBCD para a carga de um TFMTBCDField, e uma DATE lida por um TDateTimeField levanta EConvertError. Servidor antigo não manda a chave: lê -1, e o cliente monta o que sempre montou.
  • O tamanho diz se é data, hora ou os dois (RALDateTimeKind): sftDateTime sozinho não diz. Amarrada como data e hora, uma hora chegaria numa coluna TIME do SQLite como '1899-12-30 hh:nn:ss.zzz', que nenhum driver lê de volta. Leitor antigo ignora o tamanho.
  • As tabelas de nomes de tipo são preenchidas na initialization, antes de qualquer thread, e depois só lidas. GetEnumName percorre a tabela RTTI e, no Delphi, devolve UTF-16 que ainda precisa virar StringRAL, por campo e por linha. As tabelas são preenchidas por GetEnumName, não à mão, porque TFieldType muda de um compilador para outro. Preenchidas na primeira chamada, elas correriam entre threads: com 6 threads, 441 de 3000 rodadas deram nome errado ou exceção, e um '' vira ftUnknown, que o servidor recusa ("Field is of an unknown type") nos primeiros pedidos DAO paralelos.
  • Enum que vem do fio se compara como Cardinal. O Delphi compara um enum de até 128 membros como byte com sinal (Win32 e Win64), então Ord(x) > Ord(High(T)) deixa passar de 128 a 255. Um case também não salva: no Win32 o 128 cai no ramo de sftShortInt, e no FPC o salto pela tabela do case dá access violation. Por isso todo valor do fio passa por RALIsFieldTypeOrdinal antes do cast.
  • RALDBResponseError em três passos. AnswerException responde com um param Exception, mas um param sozinho no corpo viaja sem nome (vira o corpo anônimo); param ausente é nil, então as leituras precisam de guarda (é o caminho de erro); e uma resposta só com status (um proxy, um Answer(status)) não pode virar uma exceção de mensagem vazia. O status vai escrito na mensagem, para quem procura pelo número encontrá-lo.

Stored procedures, transação e o fio versão 2

  • O fio versão 2 lê a versão 1 e responde nela. Um cliente antigo manda v1 e só lê v1: o cache guarda a versão em que chegou (Version) e a resposta sai nela, sem o que só a v2 tem. Um cliente 2.0 com RALLegacyWire grava v1 e recusa etProc e parâmetros de saída com exceção, em vez de deixá-los sumir.
  • A resposta v2 guarda o primeiro result set onde a v1 guardava o único (Stream, Native, ContentType); saídas e os result sets seguintes vêm depois. Assim o código que só conhece um result set continua certo.
  • O catálogo é a lista, e só o que existe fica no cache. Uma procedure que o banco não conhece não é guardada: criada depois, ela aparece na próxima chamada, sem esperar CatalogLife. A chave do cache leva os nomes de parâmetro enviados, porque no PostgreSQL eles escolhem entre sobrecargas.
  • As saídas viram um dataset do próprio driver (NewMemDataset: TFDMemTable, TBufDataset), porque qualquer storage escreve um dataset e não há um dataset em memória comum aos dois compiladores sem dependência extra. O Zeos no Delphi não tem um que sirva: lá as saídas vão em JSON para o cliente HTTP comum.
  • Tudo é escrito antes do commit. Numa transação explícita do Firebird o commit fecha os cursores; a rota grava saídas e result sets e só então confirma.
  • O Zeos 8 tem TZParams, não TParams. O driver Zeos liga parâmetros pelo ParamByName da query, nunca pelo tipo da lista, para valer no Zeos 7 e no 8.
  • A fila do cliente não prende conexão. StartTransaction no TRALDBConnection só junta os comandos; o Commit manda um pedido applyupdates com a marca de transação, e o servidor roda tudo numa transação que acaba com o pedido. Por isso um comando da fila não lê o resultado de outro, e o Open é recusado lá dentro.
  • O nome do parâmetro de rota é ral_procname, não name: os parâmetros da procedure chegam pela query string também, e uma procedure pode ter um parâmetro chamado name.

Drivers, pool e datasets remotos 🚧

  • SQLite espera o arquivo ocupado. O SQLite recusa na hora uma segunda conexão que toca um arquivo ocupado ("database is locked"). O FireDAC espera até 10 s por padrão; o driver sqldb põe o mesmo sqlite3_busy_timeout no handle logo depois de abrir, e o Zeos pede o mesmo nas propriedades quando a aplicação não pediu outro. Assim um SQLite no pool se comporta igual em todo driver.
  • sqldb abre uma conexão por vez (gOpenLock): abrir carrega a biblioteca cliente e conta referências nela (sqlite3dyn, ibase60dyn, o SQLDBLibraryLoader), e essa contagem não é thread-safe. Uma abertura por vez não custa nada perto do pedido.
  • sqldb grava cada comando na hora, como o FireDAC e o Zeos com AutoCommit: um commit que falha (constraint adiada, conflito de serialização) tem de chegar à resposta, e não depois dela.
  • O pool separa a conexão morta sem liberá-la debaixo da trava: liberar desconecta, o que é E/S de rede (um banco que caiu espera um timeout de TCP), e todo Acquire e Release ficaria na fila. Conectar também fica fora da trava; quem solta a trava chama FreeDead. Uma conexão que não está conectada é desconectada antes de reconectar, porque o Zeos guarda o handle de uma conexão que caiu e não reabre com ele lá.
  • A query de um pedido é liberada ali mesmo quando algo falha (param desconhecido, valor que não converte, comando recusado): ninguém mais a segura, e o pool mantém a conexão viva.
  • O faReadonly não é copiado para a memtable do Zeos: o servidor marca toda coluna sem coluna de origem (um CAST(x) AS, um SUM(), qualquer expressão), e o TZMemTable aplica o somente leitura em toda escrita, inclusive na carga do storage. O Required é copiado: ele é sobre o dado, não sobre quem escreve.
  • Stream nativo do FireDAC traz o próprio esquema: a memtable descarta os campos que adivinhou pelo mapa de tipos do RAL e deixa o FireDAC usar os do stream (FLoadingNative); BCD nativo carregado num campo de outro tipo é lido como lixo. Os campos persistentes do Fields Editor ficam, porque são do form: um persistente criado com o tipo errado para uma coluna NUMERIC para a carga com erro de tipo, e tem de ser recriado com o tipo certo.
  • Sem exportação nativa no Zeos de fábrica: o ZeosLib vem com ZMEMTABLE_ENABLE_STREAM_EXPORT_IMPORT comentado no Zeos.inc, então no Delphi o TZMemTable não importa stream. O driver responde que não exporta nativo, e o servidor usa o formato de storage do RAL, que os dois lados leem.
  • AffectedRows é lido por nome e depois sem nome no DAO do FireDAC: o applyupdates e o execsql respondem com um param de corpo só, que viaja cru e chega como ral_body; o opensql responde com dois params, e o multipart guarda os nomes.
  • Um 200 que não é resposta de query vira erro pelo OnError, como um 500: levantado dentro do callback ele se perderia na thread da resposta (ou escaparia do Open) e deixaria o dataset marcado como carregando, sem poder abrir de novo. O mesmo vale para uma exceção antes do pedido (sem Client, motor não registrado): a marca é baixada num finally.
  • Os datasets remotos guardam o último /getsqlfields e o SQL que ele descreve (FSchema), para um Open não perguntar de novo antes do /opensql; trocar de conexão ou de SQL descarta o cache.

Clone this wiki locally