SQL malformatado é problema de revisão, não de gosto
SQL é a última parte da maioria dos repositórios em que ainda se discute formatação em vez de automatizá-la. Código de aplicação passa por Prettier, gofmt, Black ou rustfmt antes de virar pull request. Já a consulta enfiada três níveis abaixo num arquivo, ou a migração que alguém colou direto do cliente do banco, chega do jeito que foi digitada: uma linha de 900 caracteres, ou quarenta linhas indentadas do jeito que o editor achou melhor.
A conta aparece na revisão. Quem lê uma consulta sem formatação não enxerga o formato dela —
quais condições pertencem ao ON e quais ao WHERE, se aquele
OR está dentro ou fora dos parênteses, quantas tabelas realmente entram no join.
E são exatamente esses os erros que produzem uma consulta que roda, devolve linhas
plausíveis e está errada. Um OR que escapou dos parênteses não levanta exceção
nenhuma: ele devolve a tabela inteira, calado.
Formatação consistente também devolve utilidade ao controle de versão. Quando toda consulta
do repositório quebra linha nos mesmos lugares, mudar uma condição gera um diff de uma linha.
Quando não quebra, acrescentar uma coluna ao SELECT reescreve a instrução
inteira no diff, e quem revisa precisa reler tudo para descobrir o que mudou. Esse é o
argumento prático mais forte para passar SQL por um formatador — não a beleza, e sim o
tamanho do diff.
As convenções de estilo que brigam entre si
Não existe guia de estilo oficial para SQL, então cada time herda a convenção de quem escreveu a primeira migração. Quatro discussões se repetem em todo lugar, e vale entender o que cada lado está de fato otimizando.
| Escolha | Argumento a favor | Argumento contra |
|---|---|---|
| Palavra-chave em maiúscula | Separa palavra reservada de identificador sem depender de destaque de sintaxe — num diff, num log, num terminal | Parece grito; irrelevante se o editor já colore |
| Vírgula à frente | Comentar ou apagar uma linha nunca quebra a linha de cima | Estranha para quem aprendeu do outro jeito |
| Alinhamento em coluna (rio) | Separa visualmente a coluna de palavras-chave da coluna de valores | Cada renomeação realinha o bloco inteiro e incha o diff |
| Uma condição por linha | Cada predicado vira uma linha independente e revisável | Consulta curta fica alta sem ganho nenhum |
O argumento da vírgula à frente é o mais subestimado, então vale destrinchar. Com vírgula ao fim, o último item da lista não tem vírgula. Comente essa última linha e a linha de cima passa a terminar com vírgula solta, o que é erro de sintaxe na maioria dos bancos: para remover uma linha você teve que editar duas. Com vírgula à frente, a vírgula pertence ao item que vem depois dela, então toda linha se basta — comente qualquer uma, inclusive a última, e a lista restante continua válida. Quando você está caçando o problema de um relatório removendo coluna por coluna, essa diferença é o jogo inteiro.
O contra-argumento também é real: vírgula à frente parece errada até deixar de parecer, e misturar os dois estilos no mesmo repositório é pior que qualquer um dos dois. Por isso a posição da vírgula aqui é uma chave, e não uma opinião embutida na saída — escolha a que seu time já usa e o formatador deixa de ser fonte de ruído no diff.
Diferenças de dialeto que mudam a formatação
A maior parte das palavras-chave é comum a todos os bancos. O que não é comum é como cada um delimita as coisas — e delimitador é exatamente o que um formatador precisa acertar. Uma suposição errada transforma nome de coluna em string, ou string em código.
- Aspas duplas. No SQL padrão, no PostgreSQL, no Oracle e no SQL Server,
"total"é um identificador delimitado — uma coluna com exatamente esse nome, caixa inclusa. No MySQL com a configuração de fábrica,"total"é uma string. Os mesmos três caracteres, sentidos opostos. Um formatador que assume um dos dois troca alegremente a caixa do miolo do outro. - Crase e colchete. O MySQL delimita identificador com
`nome`; o SQL Server usa[nome], em que o escape do colchete literal de fechamento é]]. Nenhum dos dois existe no outro banco, e tratar[como delimitador no PostgreSQL quebraria índice de array. - Comentários.
--e/* */são universais.#como comentário de linha é só do MySQL — e no SQL Server o mesmo caractere começa nome de tabela temporária,#tmp. O PostgreSQL é a exceção nos comentários de bloco: lá eles aninham, então/* a /* b */ c */é um comentário só, e é erro de sintaxe em quase todo o resto. - Escape de string. Duplicar a aspa,
'it''s', é o padrão e funciona em todo lugar. O MySQL aceita também barra invertida; o PostgreSQL só dentro de um literalE'...', e ainda acrescenta a string com cifrão —$$ ... $$— em que o delimitador é escolhido por quem escreve e o corpo pode conter qualquer coisa, aspas e marcadores de comentário inclusive. - Operador de conjunto e paginação. O Oracle chama a diferença de
MINUS; todo o resto chama deEXCEPT. Paginação éLIMITno PostgreSQL e no MySQL,TOP nno SQL Server eFETCH FIRST n ROWS ONLYno padrão — três lugares diferentes para quebrar a linha.
Como este formatador evita corromper a sua consulta
Todo bug de formatador que importa tem a mesma forma: alguma coisa dentro de um literal foi
tratada como código. O relato clássico é um WHERE observacao = 'SELECT FROM' que
volta reformatado, com a caixa trocada ou quebrado em duas linhas — uma consulta que continua
rodando e agora não casa com nada. Por isso a primeira coisa que acontece aqui é uma
varredura completa de tokenização, antes de qualquer decisão de layout.
O tokenizador percorre o texto caractere a caractere com estado explícito. Dentro de uma string ele sabe que duas aspas seguidas são uma aspa escapada e não o fim do literal — que é exatamente o ponto em que implementações ingênuas terminam cedo e corrompem tudo dali em diante. Dentro de um comentário de linha ele sabe que um apóstrofo é só um apóstrofo. Dentro de um identificador delimitado ele sabe qual caractere de fechamento procurar, e isso depende do dialeto que você escolheu. Só tokens classificados como palavra sem delimitador podem ter a caixa alterada, e só se estiverem na lista de palavras reservadas.
Nome de função tem uma condição a mais: a caixa só muda quando vem um ( colado.
Muito schema real tem coluna chamada count, replace ou
position, e não há motivo para gritar com elas.
Duas consequências vêm de tratar isto como problema de segurança e não de renderização. A primeira: se o tokenizador não consegue terminar — string sem fechar, comentário de bloco aberto, identificador que nunca fecha — nada é formatado. Você recebe a entrada intacta e o motivo. Uma consulta formatada pela metade, com cara de pronta, é pior que saída nenhuma, porque convida você a copiar. A segunda: depois de produzido, o resultado é tokenizado de novo e comparado token a token com a entrada. Se algo se perdeu, duplicou ou foi reinterpretado, o resultado é descartado e o original volta. Formatar é conveniência; corromper uma consulta que depois roda em produção é prejuízo de verdade, e a troca não compensa.
Lendo a estrutura: CTE, subconsulta, JOIN … ON e CASE
A indentação se paga em quatro lugares, e é neles que a complexidade deste formatador foi gasta.
Subconsulta e CTE ganham um nível de aninhamento. Um parêntese cujo primeiro
token relevante é SELECT ou WITH vira bloco: abre no fim da linha
atual, o conteúdo desce um nível e o parêntese de fechamento fica sozinho na indentação da
linha que o abriu. Parêntese de chamada de função e de lista IN (…) continua na
mesma linha, porque quebrá-lo acrescenta altura sem acrescentar informação. É essa distinção
que faz uma consulta com três CTEs ser legível de relance — dá para ver onde cada bloco
começa e termina sem contar parênteses.
JOIN … ON coloca o join em linha própria no nível da cláusula e a condição
um nível adiante. Quando o join tem várias condições, cada AND cai numa linha
abaixo do ON, e fica imediatamente visível se um predicado pertence ao join ou
ao WHERE — distinção que muda o resultado em outer join e que é invisível numa
consulta de uma linha só.
Expressão CASE coloca cada WHEN e o ELSE em linha
própria, com o END de volta ao nível do CASE. Um CASE
de cinco ramos escrito em linha é ilegível; escrito assim, vira uma tabelinha.
Lista longa de colunas quebra um item por linha, mas só quando há mais de um
item. ORDER BY criado_em DESC fica numa linha só; um SELECT de
quinze colunas vira quinze linhas. Quebrar cláusula de item único é o tipo de regra que deixa
o SQL formatado com o dobro da altura necessária.
O AND de BETWEEN … AND … não é tratado como conectivo lógico, de
propósito, porque não é um: ele faz parte do operador. Formatador que quebra nele produz uma
linha solta que se lê como se fosse outra condição.
Por que colar SQL de produção num site que faz upload é problema
A maioria dos formatadores online manda seu texto para um servidor, formata lá e devolve o resultado. Para um seletor de cores isso seria uma curiosidade. Para SQL, é divulgação de informação.
Uma consulta de produção é uma descrição compacta do seu sistema. Ela nomeia tabelas e
colunas, o que já é vazamento de schema. Ela codifica regra de negócio — como um cliente é
classificado, o que conta como cancelamento, qual flag suprime uma cobrança. E os literais do
WHERE são, com frequência, dado vivo: um id de conta, um e-mail, um CPF que
alguém estava investigando quando a consulta parou de funcionar. Essa combinação é o que uma
revisão de segurança chama de vazamento de dado — e acontece por uma caixa de texto, sem
arquivo, então nada no pipeline de DLP percebe.
A consequência prática não é que formatar seja perigoso, e sim que o lugar do processamento decide se é. Aqui o formatador é um módulo JavaScript entregue junto com a página. Não há código de rede dentro dele, e dá para conferir: abra a aba de rede, cole uma consulta, formate e veja que nada sai. Dá para carregar a página, desconectar e continuar formatando. Se uma ferramenta não sobrevive a esse teste, ela está enviando sua consulta, diga o que disser a página de privacidade.
O que esta ferramenta não faz, de propósito
Ser explícito sobre os limites é mais útil do que fingir que eles não existem:
- Não valida o SQL. Não há gramática nem schema, então consulta que referencia tabela inexistente, ou que agrupa pela coluna errada, é formatada sem reclamação. O único freio é texto que não dá para tokenizar.
- Não reescreve nem otimiza. Nenhuma condição é reordenada, nenhum join é convertido, nenhuma subconsulta é achatada. Reescrever muda o plano que o banco escolhe, e isso não é assunto de formatador.
- Não alinha colunas em rio. Alinhamento fica ótimo até a primeira renomeação, quando todas as linhas do bloco mudam e o diff vira ilegível. Indentação fixa é a escolha que mantém o diff pequeno.
- Não reindenta o miolo de comentário de bloco. O que você escreveu dentro
de
/* … */é reproduzido exatamente, com as quebras que você pôs. Reformatar o corpo do comentário também tornaria a formatação não idempotente — e formatar a mesma consulta duas vezes tem que dar sempre o mesmo resultado. - Não trata corpo de procedure com estrutura real. Blocos
BEGIN … ENDlongos, cursores e controle de fluxo são tokenizados com segurança e diagramados de forma razoável, mas um formatador de instrução não é um formatador de linguagem procedural e não vai fingir que é.
Perguntas frequentes
Minha consulta é enviada para algum servidor?
Não. O tokenizador e o formatador são JavaScript que vem junto com a página e roda dentro da sua aba. Não há upload, nem chamada de API, nem registro. Isso pesa mais em SQL do que em qualquer outro formato: uma consulta expõe nomes de tabela, nomes de coluna e boa parte da regra de negócio, e costuma carregar dado real de cliente dentro do WHERE.
A formatação pode mudar o que a consulta faz?
O desenho é justamente para que não mude. A formatação só reescreve espaço em branco e a caixa das palavras reservadas; conteúdo de string, de comentário e de identificador delimitado é copiado byte a byte. Antes de mostrar qualquer coisa, a ferramenta tokeniza a própria saída e compara token a token com a entrada. Se um único token divergir, o resultado é descartado e o SQL original volta com uma explicação.
E se meu SQL tiver erro de sintaxe?
Depende do tipo de erro. Isto é um formatador, não um parser com gramática: um GROUP BY errado ou um alias faltando continuam sendo formatados, porque a ferramenta não tem como saber que a consulta está errada. O que ela recusa é o que não consegue tokenizar com segurança — string sem fechar, comentário de bloco aberto, identificador cujo delimitador nunca fecha. Nesses casos nada é alterado e você recebe o motivo.
Qual dialeto eu devo escolher?
O do banco em que a consulta realmente roda. A opção não é cosmética: ela decide o que significa aspas duplas (identificador em todo lugar, menos no MySQL padrão, onde é string), se crase e colchete delimitam identificador, se # começa comentário e se existe string com cifrão. Escolher o dialeto errado é a maneira mais rápida de um formatador estragar um literal.
Por que o Minificar mantém uma quebra de linha depois do comentário?
Porque o comentário -- vale até o fim da linha. Colapsar numa linha só uma consulta que tem comentário de linha comentaria tudo o que vem depois dele, transformando uma instrução que funciona numa que não roda. Por isso o minificador mantém exatamente uma quebra depois de cada comentário de linha e remove todas as outras. Comentário de bloco não tem esse problema e continua embutido.