Migração chaves de 64 bits

Historicamente as chaves e versões dos registros do sistema eram representados por inteiros de 32 bits, podendo assumir valores entre −2,147,483,648 e 2,147,483,647. Enquanto esses limites podem ser considerados suficientes para a maioria das bases de dados, há alguns tipos de operações que requerem a criação de milhões de registros por dia, tornando esses limites uma restrição do tempo de vida de uma base de dados.

Para eliminar essa restrição, o Engine, a partir da versão 41, passou a tratar chaves e versões como inteiros de 64 bits. Apesar dos inteiros de 64 bits poderem assumir valores entre -9.223.372.036.854.775.808 e 9.223.372.036.854.775.807, o sistema adota uma faixa de valores menor, de -9.007.199.254.740.991 (Number.MIN_SAFE_INTEGER) a 9.007.199.254.740.991 (Number.MAX_SAFE_INTEGER). O uso dessa faixa menor se deve ao tipo Number do JavaScript e à sua
limitação de não permitir armazenar valores fora dessa faixa sem perda de precisão. Mesmo com essa restrição de uso, a capacidade das chaves e versões 64 bits possui uma ordem de grandeza seis vezes superior à das chaves e versões de 32 bits, expandido de forma significativa os limites de utilização do sistema.

Apesar do Engine passar a suportar o novo tamanho de chaves e versões, esse novo recurso estará desativado por padrão até que as tabelas das bases de dados sejam convertidas para utilizar o novo tipo de dado e os códigos fontes sejam revistos para tratar adequadamente o novo tipo. Também é importante observar que durante um período de tempo, haverá bases utilizando chaves e versões de 32 bits enquanto outras já terão sido migradas para 64 bits. Por esse motivo, os códigos-fontes dos produtos do sistema devem ter a preocupação de preservar a compatibilidade com as bases de dados que não foram migradas.

De forma sucinta, a migração de uma base de dados consiste em:

  1. Revisar os códigos-fontes, incluindo as definições do modelo de dados.
  2. Converter as tabelas da base de dados.
  3. Ativar a geração de chaves 64 bits.

A seguir são detalhadas as práticas que devem ser adotadas para os novos códigos-fontes, além dos pontos que devem ser revistos nos códigos existentes. Também serão relacionadas as estratégias de migração das bases de dados existentes.

Tipos de dados

O tipo integer na definição de campos de tabelas e grades passa a ser um tipo legado e em seu lugar foram criados os tipos:

  • int32: tipo inteiro de 32 bits que deve ser utilizado exclusivamente para representar quantidades no intervalo de −2,147,483,648 e 2,147,483,647. Campos do tipo int32 são criados com o tipo SQL integer na base de dados.
  • int64: tipo inteiro de 64 bits que deve ser utilizado para armazenar valores de chaves, versões e quantidades que ultrapassem os limites de um int32. Campos do tipo int64 são criados com o tipo SQL bigint na base de dados.

É de se esperar que haja um uso muito mais frequente do tipo int64 do que do tipo int32. O tipo int32 deve ser utilizado apenas como uma forma otimizada de armazenar quantidades de pequena ordem de grandeza, como os sucessos e as falhas de execução de testes unitários, enquanto o int64 será utilizado em todos os campos que armazenam chaves, versões e quantidades com uma ordem de grandeza maior, como timestamps de datas.

No uso do DataSet, o tipo legado integer será mapeado automaticamente para os tipos int64 e int32 de acordo com o valor retornado pela função DataSet.getIntegerDataType(). Por padrão, o tipo integer será considerado um int64.

Nos campos do modelo de dados e nas grades, o tipo integer será mapeado para os tipos SQL integer ou bigint de acordo com o valor da propriedade ModelDef.prototype.integerDatabaseType. Na classe Raiz esse tipo atualmente está definido como integer, sendo esse o valor adotado por padrão em todos campos que não foram revistos para utilizar os tipos int32 e int64.

Importante: para fins de compatibilidade, o método DataSet.prototype.getFieldType continuará retornando o valor "INTEGER" mesmo que o tipo do campo seja alterado para int64 ou int32. Para obter o tipo real do campo, deve ser utilizado o método DataSetFieldDefs.prototype.get.

Pelo mesmo motivo, o método Field.prototype.isInteger retorna true independentemente do tipo de inteiro adotado. Para saber o tipo real do campo, podem ser utilizados os métodos Field.prototype.isInt64 e Field.prototype.isInt32.

Criação de novos campos no sistema

A partir da versão 41 do Engine, todos os campos criados que sejam lookups simples ou armazenem chaves ou versões devem ser declarados com o tipo int64, conforme exemplo abaixo:

var fld = this.field('UF', 'int64');
fld.classKey = -1899999722;

Lookups múltiplos devem continuar a ser declarados com o tipo memo.

Não há benefício em utilizar o tipo legado integer em novas colunas. Criar uma coluna desse tipo apenas aumenta a massa de dados que precisará ser convertida para bigint no futuro.

Revisão dos códigos-fontes

Os códigos-fontes que definem o modelo de dados, processos, relatórios, fontes de dados e bibliotecas precisam ser revistos para levar em consideração o novo tamanho das chaves e versões do sistema. Essa revisão precisa ser realizada em todos os códigos do sistema, incluindo customizações.

A revisão da base de dados para suportar o uso das chaves de 64 bits não afeta significativamente os códigos existentes enquanto as chaves e versões geradas pelo sistema forem inteiros de 32 bits. No entanto, a ativação da geração de chaves e versões 64 bits sem a devida revisão dos códigos-fontes pode levar ao truncamento de valores, provocando perda de dados e falhas de integridade que não podem ser corrigidas posteriormente.

Para realizar a revisão de códigos é sugerido o uso da extensão do VS Code, pois ela permite a pesquisa diferenciando minúsculas de maiúsculas, assim como o uso de expressões regulares. É recomendado que sejam revistos primeiro os arquivos dos pacotes JAZ, que normalmente contêm as bibliotecas e objetos de negócio, e em seguida os arquivos da VFS.

Revisão do modelo de dados (arquivos x-model, x-view e x-class)

Os tipos dos campos definidos em arquivos do tipo x-model, x-view e x-class devem ser revistos considerando os dados que são gravados neles e a quantidade de registros contidos nas tabelas:

  • Campos que armazenam quantidades de pequena ordem de grandeza devem ter o seu tipo alterado de integer para int32. Essa modificação garante que o campo terá o seu tipo preservado durante a conversão dos dados e também evita um custo de armazenagem desnecessário no SGBD, principalmente para o caso do PostgreSQL. Nesse SGBD, a quantidade máxima de colunas por tabela varia de acordo com o tamanho do tipo das colunas e, por esse motivo, deve-se evitar o uso do tipo int64 quando um tipo de menor tamanho é suficiente. Essa recomendação também se aplica aos campos do tipo combo, quando as opções forem valores inteiros pequenos, como os dias da semana ou os valores de um enumerado.
  • Campos que armazenam chaves, versões e quantidades de maior ordem de grandeza devem ter o seu tipo alterado para int64 nas tabelas com menor volume de registros e que podem ser convertidas automaticamente pelos processos de atualização. Nas tabelas maiores, na ordem de milhões de registros, o tipo deve ser preservado como integer, permitindo que a sua conversão seja realizada de forma controlada, base-a-base, por meio da propriedade ModelDef.prototype.integerDatabaseType.

É importante observar que há campos que armazenam chaves e versões, mas que não são definidos como campos lookup, como os campos que armazenam chaves de criação, unificadores, vínculos acessórios e chaves de eventos. Por esse motivo, a revisão não deve ser restrita apenas aos campos com as propriedades classKey e lookupType definidas. Caso haja dúvida se um campo armazena uma chave ou uma quantidade, é melhor considerar o tipo mais amplo e tratá-lo como um inteiro de 64 bits.

Nas tabelas com menor volume de registros, que foram revistas para utilizar explicitamente os tipos int64 e int32, é recomendado que a propriedade integerDatabaseType da classe associada à tabela seja alterada para o valor 'bigint'. Dessa forma, as eventuais customizações que não tenham sido revistas também serão convertidas automaticamente pelos processos de atualização.

Sugestões de pesquisa no código-fonte para auxiliar a revisão:

  • ['"]integer['"]: com a opção “Use Regular Expression” ativa. A princípio a opção “Match case” poderia estar ativa, mas há casos conhecidos de campos definidos com outras variações, como “Integer”, valores hoje aceitos pelo sistema. É recomendado que essas variações sejam revistas para utilizar o nome do tipo em caixa baixa.
  • ['"]combo['"]: com a opção “Use Regular Expression” ativa. Caso os valores das opções sejam números inteiros pequenos, substituir pelo tipo int32. Essa alteração exige que as atribuições do campo modificado sejam sempre pelo valor, pois apenas o tipo combo aceita o índice de uma opção como valor válido. Mais detalhes em GridField.prototype.setValue.

Revisão dos demais códigos-fontes

Como o tipo integer no DataSet já está sendo mapeado para o tipo int64, o foco da revisão deve ser as eventuais conversões de dados realizadas em SQL e a armazenagem, exportação e exibição de chaves em outros locais que não sejam um campo de um DataSet ou uma grade do sistema.

Enquanto uma chave de 32 bits pode ser representada por um texto de 11 caracteres, uma chave ou versão de 64 bits, desconsiderando a restrição atual do tipo Number do JavaScript, pode requerer até 20 caracteres, sendo 19 dígitos e um caractere para o sinal.

Sugestões de pesquisa no código-fonte para auxiliar a revisão, todas com a opção “Use Regular Expression” ativa:

  • ['"]integer['"]: deve ser verificado se não deveriam ser utilizados os tipos int64 e bigint.
  • as\sinteger: deve ser substituído por AS BIGINT se for uma conversão de chave ou versão, ou nos casos em que a consulta tenta criar um campo vazio que será utilizado para gravar uma chave, como CAST(null as INTEGER) AS CLASSE ou CAST(0 as INTEGER) AS CLASSE.
  • as\svarchar(10) e as\svarchar(11): caso seja uma conversão de uma chave para texto, ela deve ser alterada para utilizar o tipo VARCHAR(20). Observar que o retorno dessa conversão também precisa ser revisto, como um eventual uso da função SQL SUBSTR e o uso do campo retornado pela consulta.
  • \+\s1000000000: a soma de uma chave com o valor 1000000000 é utilizada em estratégias para converter uma chave em uma string com zeros à esquerda. Essas lógicas devem ser revistas para prever o tamanho máximo de 20 caracteres das chaves.

Outros pontos de atenção:

  • Uso de chaves na maioria dos códigos de barras lineares (1D), como o EAN-13, não será mais possível. Deve ser revisto o uso da classe BarCode, da URL “/createBarCode” e de funções que gerem códigos a partir de chaves, como eanInternoDaChave e chaveDoEanInterno. Em vez das chaves, devem ser utilizados identificadores mais curtos associados aos registros. Caso seja necessário alterar a geração de um código de barras, deve ser revisto também o processo que permite a entrada do código gerado para tratar o identificador adotado.
  • Sistemas desenvolvidos em plataformas que não sejam o Engine, como aplicativos mobile, PDVs e web services Java, precisam ser revistos caso eles manipulem chaves e versões do sistema.
  • Campos textuais que armazenam chaves, como o campo DATAHORACHAVE utilizado nas tabelas de movimentação de estoque e de eventos, devem ser revistos para comportar chaves de até 20 caracteres.
  • Processos de exportação e importação de dados via arquivos textuais com leiautes com campos de tamanho fixo, como os adotados pelo SPED, devem ser revistos caso as chaves e versões sejam utilizadas como valores dos campos.

Conversão das tabelas da base de dados

Após a revisão do modelo de dados, as tabelas com menor volume de registros serão convertidas automaticamente pelos processos de atualização e o foco da migração será a conversão das tabelas maiores, como as tabelas de movimentação e a de log transacional.

A conversão dessas tabelas exigirá uma janela de manutenção de maior duração que deverá ter o seu tempo estimado em uma base de homologação. Essa base deve ser uma réplica fiel da base de produção, incluindo a tabela iLog.

É importante que todas as tabelas de soma sejam removidas antes de iniciar a conversão das tabelas. Para isso, todos os registros da grade “Visões criadas”, exibida no processo “Desenvolvimento > Base de dados > Tabelas de soma”, devem ser removidos. As tabelas de soma devem ser recriadas apenas após todas as tabelas terem sido convertidas.

Para habilitar a conversão dos dados deve ser criado o arquivo “9900 IntegerDatabaseType.model” na classe Raiz, inicialmente apenas na base de homologação, com o conteúdo abaixo:

this.integerDatabaseType = 'bigint';

Feita essa configuração, deverá ser executado o processo “Desenvolvimento > Base de dados > Atualizar estrutura”, aplicando todas as alterações sugeridas. Com base no tempo observado, há três estratégias possíveis:

  1. Caso seja possível interromper a base de produção pelo período observado, é recomendado que a conversão de todas as tabelas seja realizada em uma única janela de manutenção. É importante observar que a base de produção não poderá ser utilizada durante o processo de conversão.
  2. Caso não seja possível interromper a base de produção pelo período necessário para converter todas as tabelas, pode-se medir o tempo de conversão de cada tabela individualmente e realizar a conversão por partes. Para isso, basta configurar a propriedade integerDatabaseType nas classes que definem as tabelas em vez da classe Raiz.
  3. Caso a conversão de uma única tabela ultrapasse a janela máxima de manutenção permitida na produção, deve-se criar uma cópia da base de produção, converter os dados e sincronizar a cópia com a produção utilizando o log transacional. Uma vez sincronizada, a cópia deve substituir a base de produção. Essa estrátegia é mais complexa e exige que todos os dados gravados no sistema gerem log transacional. O uso do applyUpdates deve ser revisto para confirmar que a opção log não está sendo desativada. Devido ao risco dessa revisão, essa estrátegia deve ser utilizada como última opção.

Observações:

  • A conversão do tipo integer para bigint no Oracle é uma ampliação simples da precisão do tipo decimal, sendo uma operação rápida e de baixo custo. Já no PostgreSQL e no Microsoft SQL Server, a conversão exige a reescrita da tabela e o SGBD deve ter espaço suficiente para duplicar o tamanho das tabelas convertidas.

Ativação da geração de chaves 64 bits

Após a revisão dos códigos-fontes e conversão das tabelas da base de dados, a geração de chaves 64 bits pode ser ativada por meio do processo “Admin > Base de dados > Migração chaves 64 bits”. Esse processo verifica se ainda existem campos lookup com o tipo “int32” no modelo de dados e, caso não haja, permite que o administrador ative a geração das chaves 64 bits. É importante observar que essa verificação não elimina a necessidade de revisão dos códigos-fontes, pois há campos que armazenam chaves e versões que não são configurados como lookup e o uso dos campos também precisa ser revisto, levando em consideração o novo tamanho das chaves e versões.

É recomendada que a ativação da geração de chaves 64 bits ocorra primeiramente em uma base de testes. Somente após a homologação do sistema nessa base de testes, ela deve ser ativada no ambiente de produção.

A ativação das chaves 64 bits consiste em permitir que o Engine gere chaves com valores superiores a 2.147.483.647, no entanto, chaves nessa ordem de grandeza serão geradas apenas quando as faixas de chaves inferiores forem esgotadas, não alterando assim o comportamento do sistema de forma imediata. Apenas nas bases de testes e homologação, podem ser executados os comandos abaixo para forçar a geração de chaves altas a fim de validar se o sistema está tratando adequadamente o novo tamanho das chaves e versões:

  // Execute apenas nas bases de testes e homologação, jamais na produção, pois
  // não é possível desfazer a alteração dos campos de versão.
  database.executeSQL("UPDATE iKeyRange SET iEnabled = CASE WHEN iStart >= 900000000000000 THEN 'T' ELSE NULL END");
  database.executeSQL("UPDATE ULTCHAVE SET VERSAO = 900000000000000, VERSAOFT = 900000000000000, VERSAOBD = 900000000000000");

Após a execução dos comandos acima, os Engines devem ser reiniciados para que os seus caches de dados e chaves sejam descartados.