Eu tenho um arquivo de dados de exemplo com o seguinte conteúdo e salvo com codificação UTF8.
oab~opqr
öab~öpqr
öab~öpqr
O formato deste arquivo é de largura fixa com as colunas 1 a 3 sendo cada uma alocada com 1 caractere e a coluna 4 reservada com 5 caracteres.
Eu criei um arquivo de formato XML como abaixo
<?xml version = "1.0"?>
<BCPFORMAT xmlns="http://schemas.microsoft.com/sqlserver/2004/bulkload/format" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<RECORD>
<FIELD xsi:type="CharFixed" ID="Col1" LENGTH="1"/>
<FIELD xsi:type="CharFixed" ID="Col2" LENGTH="1"/>
<FIELD xsi:type="CharFixed" ID="Col3" LENGTH="1"/>
<FIELD xsi:type="CharFixed" ID="Col4" LENGTH="5"/>
<FIELD xsi:type="CharTerm" ID="LINE_BREAK" TERMINATOR="\n"/>
</RECORD>
<ROW>
<COLUMN SOURCE="Col1" NAME="Col1" xsi:type="SQLNVARCHAR"/>
<COLUMN SOURCE="Col2" NAME="Col2" xsi:type="SQLNVARCHAR"/>
<COLUMN SOURCE="Col3" NAME="Col3" xsi:type="SQLNVARCHAR"/>
<COLUMN SOURCE="Col4" NAME="Col4" xsi:type="SQLNVARCHAR"/>
</ROW>
</BCPFORMAT>
Lamentavelmente executando o seguinte SQL ...
SELECT *
FROM OPENROWSET
(
BULK 'mydata.txt',
FORMATFILE = 'myformat_file.xml',
CODEPAGE = '65001'
) AS X
Produz os seguintes resultados
Col1 Col2 Col3 Col4
---- ---- ---- -----
o a b ~opqr
� � a b~öp
� � a b~öp
do qual concluo que LENGTH
está contando bytes em vez de caracteres.
Existe alguma maneira de fazer isso funcionar corretamente para larguras de caracteres fixas com codificação UTF8?
(O ambiente de destino é a leitura do Banco de Dados SQL do Azure do armazenamento de BLOBs)
NB: Foi sugerido nos comentários que adicionar COLLATION="LATIN1_GENERAL_100_CI_AS_SC_UTF8"
os FIELD
elementos pode ajudar, mas os resultados permanecem inalterados com isso.
Uma solução alternativa é apenas alterar o arquivo de formato para trazer a linha inteira, em massa, e fazer a substring em TSQL
Com arquivo de formato
O seguinte retorna os resultados desejados
Isso está correto e não há como transformá-lo em caracteres.
A situação é análoga ao n em char( n ), varchar( n ), nchar( n ) e nvarchar( n ), onde 'n' denota o número de bytes , não caracteres. Veja a documentação :
Isso é uma fonte de confusão para muitos, especialmente desde a introdução do suporte a UTF-8. Era possível antes com n(var)char e caracteres suplementares, mas relativamente raramente encontrado, eu diria.
Seria bom se o SQL Server estendesse seu suporte para caracteres em vez de bytes em várias áreas no futuro (incluindo OPENROWSET).
Enquanto isso, sua solução alternativa é a que eu provavelmente usaria também.
Só para colocar isso como mais uma opção, principalmente para quem não pode modificar o processo dentro do SQL Server:
Você também pode converter a codificação do arquivo de UTF-8 para UTF-16 LE (Little Endian; muitas vezes referido simplesmente como "Unicode" em muitos produtos da Microsoft). Você alteraria o seguinte (do que está postado na pergunta):
No arquivo de formato:
xsi:type
deCharFixed
paraNCharFixed
LENGTH
(ex 2 -> 4, 5 -> 10)<FIELD ID="LINE_BREAK" ...>
:xsi:type
deCharTerm
paraNCharFixed
TERMINATOR="\r\n
LENGTH="4"
(use "4" para "\r\n" ou 2 para "\n")Na chamada para OPENROWSET():
, CODEPAGE = '65001'
como página de código é ignorado ao usar o "NChar*"xsi:type
s.NOTAS
Precisar dobrar o
LENGTH
valor é uma evidência ainda mais (e mais triste) queLENGTH
é sempre bytes. Triste que eles não fizeram unidades de código para que umLENGTH
de "1" recebesse qualquer caractere BMP, exatamente como você esperaria deNCHAR(1)
/NVARCHAR(1)
dentro do T-SQL.Por que mudar
CharTerm
paraNCharFixed
em vez deNCharTerm
? Porque eu não conseguiaNCharTerm
trabalhar. Meus dados de teste estavam 100% corretos, mas o usoNCharTerm
só importaria a primeira linha. Pode ser um bug.Embora isso trate caracteres que em UTF-8 são 2 ou 3 bytes, nem essa opção nem UTF-8 manipulam caracteres combinados. Ou seja, o
ö
nos dados de amostra pode ser um único caractere (como nos dados de amostra na pergunta) ou pode ser uma combinação de um sem acentoo
mais a marca diacrítica (2 caracteres, mas agora 3 bytes em UTF-8 como o diacrítico é de 2 bytes por si só, ou 4 bytes em UTF-16). Por exemplo, eu criei o novo personagem usando:e, em seguida, copie/cole a linha 3 para uma nova linha 4, trocando o primeiro caractere com o que acabei de criar e alterando o "ab" para "cd", apenas para poder distinguir claramente as linhas de entrada. Fazer isso resultou no seguinte erro:
Eu esperaria que o arquivo UTF-8 produzisse o mesmo erro com o mesmo caractere.
E para ser justo, esse cenário também quebraria a abordagem de puxar cada linha inteiramente e usar
SUBSTRING
para dividi-la, se não produzindo um erro, pelo menos corrompendo os dados comoSUBSTRING
ainda veremoso
ë
como dois caracteres separados (já que eles são).