Pragiva — Tecnologia na prática

Campo memo: texto longo que o SQL não lê direto

Campo memo no Protheus guarda texto livre, mas no SQL Server a coluna é binária e um SELECT simples devolve bytes. Memo real x memo virtual na SYP.

Protheus Tutorial 8 min de leitura

Assista o vídeo

Nesta página5 seções

Antes de começar

  • Tabela customizada criada no dicionário (no exemplo, a ZA1 — Cadastro de motoristas)
  • Acesso ao Configurador com permissão de alterar o dicionário de dados

O campo memo (tipo M) é o campo de texto livre do Protheus: observação, histórico, descrição longa — muito texto sem precisar de um campo caractere gigante. No banco, o memo de uma tabela comum fica numa coluna binária — no SQL Server, varbinary(max) — e um SELECT direto devolve bytes, não o texto. Para ler, é preciso converter.

Existe ainda um segundo tipo de memo, mais antigo, que nem fica na tabela: o memo virtual, cujo texto mora na tabela (ou na , nas tabelas dos módulos de RH). Saber qual dos dois você tem na frente decide qual consulta funciona.

Memo real e memo virtual

Memo realMemo virtual
X3_CONTEXTRV
Onde fica o textoNuma coluna da própria tabelaNa tabela SYP, quebrado em linhas
O que a tabela guardaO texto (em formato binário, no padrão)Só um código, num campo caractere à parte
Como a tela lêDireto da colunaPelo inicializador, com a função MSMM()
Exemplo no padrão, lido a partir do

No dicionário padrão do Protheus 12, a maior parte dos campos memo é real. O cadastro de produtos ainda tem vários virtuais, e é neles que a SYP aparece.

Para um campo novo, o caminho é o memo real: um campo só, sem código auxiliar, sem tabela intermediária.

A ficha do ZA1_OBS

O campo é criado como os outros, em Base de Dados > Dicionário > Arquivos, editando a tabela e incluindo o campo na pasta Campos.

Propriedade (coluna da SX3)Valor
Tipo (X3_TIPO)M
Tamanho (X3_TAMANHO)10
Título (X3_TITULO)Observacao
Contexto (X3_CONTEXT)R
Obrigatório (pasta Uso)desmarcado
Browse (X3_BROWSE)N
Pasta (X3_FOLDER)2
Proprietário (X3_PROPRI)U

O tamanho 10 é o valor usual, não uma regra. A página de tipos de dados do TDN diz que memo tem sempre tamanho 10, mas o dicionário padrão mostra outra coisa: o memo real mais comum tem 10, e há memos com 60, 80, 250 e 999 — o B1_MOPC, citado acima, tem 80. Contexto R porque é memo real. Observação é opcional por natureza, e texto longo não cabe numa coluna da lista — fora do browse também é o que o padrão faz na maior parte dos memos.

O que o X3_TAMANHO não faz num memo real é dimensionar a coluna. No banco que conferimos, memos com tamanho 10, 80 e 250 viraram todos varbinary(max): a coluna é a mesma, qualquer que seja o número. No memo virtual o tamanho tem uso concreto, e ele aparece mais abaixo.

X3_FOLDER guarda o número da pasta da tela. No cadastro de motoristas da série, a pasta 1 é “Dados do motorista” e a 2 é “Observacoes”. Como as pastas são cadastradas, em Pastas e agrupamentos.

Por que o SELECT devolve bytes

O Protheus cria o memo real como objeto binário, e no SQL Server o tipo usado é varbinary(max) (em versões antigas era image). O conteúdo continua sendo o texto digitado; só está guardado como bytes.

Confira o tipo físico da coluna no seu banco antes de escrever a consulta:

Confira no seu ambienteSQL
SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH
  FROM INFORMATION_SCHEMA.COLUMNS
 WHERE TABLE_NAME  = 'ZA1010'
   AND COLUMN_NAME = 'ZA1_OBS'

Num memo binário, a resposta é varbinary com CHARACTER_MAXIMUM_LENGTH igual a -1: é assim que o SQL Server mostra o (max). O 010 do nome da tabela é o sufixo da empresa e muda de ambiente para ambiente. Troque pelo seu.

No SQL Server Management Studio, um SELECT ZA1_OBS direto mostra um valor começando por 0x seguido de pares hexadecimais: cada par é um caractere do texto (54 é T, 45 é E, 20 é espaço). Repare no último par. Para vê-lo sem despejar o texto inteiro, peça só o último byte:

Confira no seu ambienteSQL
SELECT TOP 5 ZA1_COD,
       DATALENGTH(ZA1_OBS) AS BYTES,
       SUBSTRING(ZA1_OBS, DATALENGTH(ZA1_OBS), 1) AS ULTIMO_BYTE
  FROM ZA1010
 WHERE DATALENGTH(ZA1_OBS) > 0
   AND D_E_L_E_T_ = ' '

No banco que conferimos, todo memo preenchido terminava assim:

TEXT
ULTIMO_BYTE
0x00

Esse 00 é um caractere nulo gravado depois do texto. Se a coluna vier varbinary, converta. É a mesma conversão que as rotinas do padrão usam nas queries deles para SQL Server:

Confira no seu ambienteSQL
SELECT ZA1_COD, ZA1_NOME,
       ISNULL(CONVERT(VARCHAR(2047), CONVERT(VARBINARY(2047), ZA1_OBS)), '') AS OBS
  FROM ZA1010
 WHERE D_E_L_E_T_ = ' '

O número entre parênteses é o limite da leitura: texto maior que 2047 caracteres vem cortado. VARCHAR com número aceita no máximo 8000; acima disso o SQL Server recusa a consulta. Para observação longa, use MAX nos dois lados:

Confira no seu ambienteSQL
SELECT ZA1_COD,
       ISNULL(CONVERT(VARCHAR(MAX), CONVERT(VARBINARY(MAX), ZA1_OBS)), '') AS OBS
  FROM ZA1010
 WHERE D_E_L_E_T_ = ' '

A tabela pode criar o memo como texto longo em vez de binário — o formato que a TOTVS chama de CLOB. A escolha é por tabela: no Configurador, na edição da tabela em Base de Dados > Dicionário, o campo “Memo CLOB” fica “Sim”. A documentação avisa que a alteração precisa ser feita antes de a tabela ser criada no banco; depois disso, os campos existentes não mudam. Numa tabela CLOB, a coluna é de tipo caractere (no SQL Server, varchar(max)) e o SELECT direto já mostra o texto.

A documentação chama esse controle de X2_MEMTYPE; no dicionário do Protheus 12, a coluna da SX2 que o guarda é a X2_CLOB. O padrão trata 2 como memo binário, e é o valor da grande maioria das tabelas; 1 marca a tabela CLOB:

Confira no seu ambienteSQL
SELECT X2_CHAVE, X2_NOME, X2_CLOB
  FROM SX2010
 WHERE X2_CHAVE = 'ZA1'
   AND D_E_L_E_T_ = ' '

Ainda assim, a palavra final é do INFORMATION_SCHEMA. No banco que conferimos há tabela com X2_CLOB = '1' e memo ainda varbinary(max), o que é compatível com o caso descrito pela documentação: a definição mudou, mas a coluna já existente não.

Lendo o memo virtual na SYP

No memo virtual, a tabela guarda só um código. No produto, o campo B1_CODOBS (caractere, 6 posições) guarda a chave, e o inicializador do B1_OBS chama MSMM(SB1->B1_CODOBS,60) para montar o texto na tela.

Aqui o tamanho trabalha: o segundo parâmetro da MSMM é a largura das linhas em que o texto é quebrado, e o 60 do inicializador é o X3_TAMANHO do B1_OBS. Na receita da TOTVS para memo virtual, a gravação passa o tamanho do campo lido com TamSX3().

A SYP guarda o texto em pedaços de até 80 caracteres, um por linha, na ordem de :

ColunaConteúdo
O código gravado no campo da tabela de origem
YP_SEQA ordem do pedaço
O pedaço do texto
O nome do campo de código que a rotina informou à MSMM ao gravar

O código não nasce da tabela de origem: na inclusão sem chave, a MSMM tira um número novo da própria SYP, com GetSX8Num("SYP","YP_CHAVE"), e grava esse número no campo de código. Na leitura, ela procura só por filial e YP_CHAVE. O YP_CAMPO guarda o nome do campo de código passado na gravação — no cadastro de clientes, por exemplo, a rotina passa , o código do memo .

Para ler a observação de um produto já remontada, numa coluna só:

Confira no seu ambienteSQL
SELECT B1.B1_COD,
       STRING_AGG(
         REPLACE(RTRIM(CAST(YP.YP_TEXTO AS VARCHAR(MAX))),
                 '\13\10', CHAR(13) + CHAR(10)),
         '') WITHIN GROUP (ORDER BY YP.YP_SEQ) AS OBS
  FROM SB1010 B1
  JOIN SYP010 YP
    ON YP.YP_FILIAL  = B1.B1_FILIAL
   AND YP.YP_CHAVE   = B1.B1_CODOBS
   AND YP.D_E_L_E_T_ = ' '
 WHERE B1.B1_COD     = 'PGV001'
   AND B1.D_E_L_E_T_ = ' '
 GROUP BY B1.B1_COD

Três detalhes fazem a diferença:

  • RTRIM em cada pedaço. O YP_TEXTO é gravado completado com espaços até as 80 posições; sem o corte, os espaços entram no meio do texto.
  • REPLACE do \13\10. Quando o texto tem quebra de linha, a MSMM não grava a quebra: grava os seis caracteres \13\10 no fim do pedaço. A consulta troca esse marcador pela quebra de verdade. Há um segundo marcador: quando um pedaço tem só espaços, a MSMM grava os espaços seguidos de \14\10, e na leitura troca esse marcador por seis espaços. Se o seu texto tiver trechos em branco, aninhe mais um REPLACE na remontagem, REPLACE(REPLACE(RTRIM(...), '\14\10', SPACE(6)), '\13\10', CHAR(13) + CHAR(10)); sem ele, o \14\10 aparece literal no resultado.
  • CAST para VARCHAR(MAX). Sem ele, quando o texto remontado passa de 8000 bytes, a consulta não devolve texto cortado: ela falha com o erro STRING_AGG aggregation result exceeded the limit of 8000 bytes. Use LOB types to avoid result truncation.

No SQL Server Management Studio, a grade de resultados não mostra a quebra de linha que o REPLACE devolveu: a documentação da Microsoft diz que o modo “Results to Grid” “can’t implement the carriage return”. Para ver o texto como foi digitado, mude para Resultados em Texto (que corta cada coluna em 256 caracteres, a menos que você aumente esse limite nas opções) ou copie a célula e cole num editor.

A STRING_AGG existe a partir do SQL Server 2017 e funciona em qualquer nível de compatibilidade do banco; a ordenação com WITHIN GROUP exige nível 110 ou maior. O JOIN pela filial repete o que a MSMM faz; se a SYP for compartilhada no seu ambiente e o produto não, troque a comparação pelo valor de gravado.

Para conferir as linhas cruas antes de agrupar, incluindo o YP_CAMPO:

Confira no seu ambienteSQL
SELECT YP.YP_FILIAL, YP.YP_CHAVE, YP.YP_SEQ, YP.YP_CAMPO, YP.YP_TEXTO
  FROM SB1010 B1
  JOIN SYP010 YP
    ON YP.YP_FILIAL  = B1.B1_FILIAL
   AND YP.YP_CHAVE   = B1.B1_CODOBS
   AND YP.D_E_L_E_T_ = ' '
 WHERE B1.B1_COD     = 'PGV001'
   AND B1.D_E_L_E_T_ = ' '
 ORDER BY YP.YP_SEQ

Se quiser uma proteção extra na remontagem, acrescente ao JOIN dela AND YP.YP_CAMPO = '...', com o valor que essa consulta mostrou.

Nas tabelas de RH, a tabela é a RDY

A MSMM decide onde gravar pelo módulo da tabela de origem, o X2_MODULO da SX2: nas tabelas dos módulos de RH, ela usa por padrão a RDY no lugar da SYP. A RDY tem as mesmas colunas com outro prefixo (, , , ) e mais a , com a filial da tabela de origem. Num memo virtual de RH, a consulta na SYP não encontra nada: use a RDY010, com o prefixo RDY_ no lugar de YP_.

A troca de tabela não basta. Na leitura, a MSMM não procura a RDY pela : ela usa o índice 2 da tabela, RDY_FILTAB+RDY_CHAVE+RDY_SEQ, e compara a RDY_FILTAB com a filial do registro de origem. No dicionário padrão, a RDY é compartilhada (X2_MODO = C) e a é exclusiva (X2_MODO = E); por isso, ligar RDY_FILIAL à devolve vazio. No JOIN, troque a comparação de filial por RDY.RDY_FILTAB = <filial da tabela de origem> — na SRA, RDY.RDY_FILTAB = RA.RA_FILIAL — e mantenha RDY.RDY_CHAVE igual ao campo de código do memo.

Em AdvPL, a função que lê e grava esse texto é MSMM(cChave, nTam, nLin, cString, nOpc, ...). Criar um memo virtual novo dá mais trabalho que um real, e a documentação da TOTVS descreve a receita: dois campos (um código caractere real e o memo virtual), o inicializador IIF(!INCLUI,MSMM(...),"") no memo, a gravação com MSMM na inclusão e na alteração, e a exclusão das linhas da SYP quando o registro é excluído — no exemplo oficial, tudo feito por pontos de entrada da rotina de cadastro. É mais uma razão para preferir o memo real num campo novo. O que separa campo real de virtual, para qualquer tipo, está em Campo real ou virtual.

Conferir o dicionário

Confira no seu ambienteSQL
SELECT X3_CAMPO, X3_TIPO, X3_TAMANHO, X3_CONTEXT, X3_RELACAO, X3_BROWSE
  FROM SX3010
 WHERE X3_ARQUIVO = 'ZA1'
   AND X3_TIPO    = 'M'
   AND D_E_L_E_T_ = ' '

Referências

  1. TDNtdn.totvs.com

    TDN — Tipos de dados (Dicionário de Dados)
  2. TDNtdn.totvs.com

    TDN — Campos MEMO estilo "CLOB" nas tabelas de dados
  3. TDNtdn.totvs.com

    TDN — Criação de campo Memo virtual usando SYP
  4. TDNtdn.totvs.com

    TDN — Melhoria: troca do tipo de campo MEMO para MSSQL
  5. Outrolearn.microsoft.com

    Microsoft Learn — STRING_AGG (Transact-SQL)

Continue por aqui