advpl-specialist
Skills

Embedded SQL

Escrita de queries SQL em ADVPL/TLPP usando blocos BeginSQL/EndSQL com macros especiais para TOTVS Protheus

Embedded SQL in ADVPL/TLPP

Embedded SQL permite escrever queries SQL diretamente em codigo ADVPL/TLPP usando blocos BeginSQL ... EndSQL com expressoes macro especiais. Substitui a concatenacao de strings propensa a erros (cQuery += "SELECT...") por blocos SQL legiveis, type-safe e manteníveis.

Quando Usar

  • Escrever qualquer query SQL em ADVPL/TLPP (preferir sobre concatenacao de strings)
  • Necessidade de query com filtragem adequada de filial, tratamento de delecao e nomes de tabela
  • Construir relatorios com SELECT, JOIN, GROUP BY complexos
  • Qualquer situacao onde TCQuery com strings concatenadas e usado atualmente
  • Quando legibilidade e manutenibilidade do codigo SQL importa

BeginSQL vs TCQuery (Concatenacao de Strings)

AspectoBeginSQL (Moderno)TCQuery + Strings (Legado)
LegibilidadeSQL escrito naturalmenteSQL enterrado em concat de strings
Nomes de tabela%table:SE2% automaticoRetSqlName("SE2") manual
Filtro de filial%xfilial:SE2% automaticoxFilial("SE2") manual
Filtro de delecao%notDel% automaticoD_E_L_E_T_ = ' ' manual
Binding de variaveis%exp:cVar%"'" + cVar + "'" (risco de SQL injection)
Tipos de colunascolumn X as DateTCSetField manual
ManutencaoFacil de ler e modificarDificil achar erros em strings
SQL InjectionProtegido via macrosVulneravel se nao tiver cuidado

Sempre preferir BeginSQL para codigo novo.

Sintaxe Principal

BeginSQL Alias cAlias
    SELECT columns
    FROM %table:ALIAS% ALIAS
    WHERE ALIAS.%notDel%
    AND ALIAS.FIELD_FILIAL = %xfilial:ALIAS%
    AND ALIAS.FIELD = %exp:cVariable%
    ORDER BY %Order:ALIAS%
EndSQL

Importante: O parametro alias pode ser uma string literal ou GetNextAlias():

Local cAlias := GetNextAlias()

BeginSQL Alias cAlias
    SELECT A1_COD, A1_LOJA, A1_NOME
    FROM %table:SA1% SA1
    WHERE SA1.%notDel%
    AND SA1.A1_FILIAL = %xfilial:SA1%
EndSQL

DbSelectArea(cAlias)
While !Eof()
    Conout(cAlias->A1_NOME)
    DbSkip()
EndDo
DbCloseArea()

Expressoes Macro Especiais

%table:TABLE%

Resolve o nome fisico da tabela com schema/owner adequado do banco de dados.

-- Input:
FROM %table:SA1% SA1

-- Expande para (exemplo):
FROM SA1010 SA1
-- (onde 010 = codigo da empresa, depende do ambiente)

%xfilial:TABLE%

Retorna o valor da filial (branch) atual para a tabela.

-- Input:
AND SA1.A1_FILIAL = %xfilial:SA1%

-- Expande para:
AND SA1.A1_FILIAL = '01'
-- (ou '' se a tabela nao e filtrada por filial)

%notDel%

Filtra registros logicamente deletados.

-- Input:
WHERE SA1.%notDel%

-- Expande para:
WHERE SA1.D_E_L_E_T_ <> '*'

Sempre incluir. Protheus usa delecao logica, nao fisica.

%exp:EXPRESSION%

Faz binding de variaveis ADVPL, expressoes ou resultados de funcao no SQL.

Local cCodCli := "000001"
Local cLoja   := "01"
Local dDataIni := CtoD("01/01/2026")

BeginSQL Alias cAlias
    SELECT E2_PREFIXO, E2_NUM, E2_VALOR, E2_EMISSAO
    FROM %table:SE2% SE2
    WHERE SE2.%notDel%
    AND SE2.E2_FILIAL  = %xfilial:SE2%
    AND SE2.E2_FORNECE = %exp:cCodCli%
    AND SE2.E2_LOJA    = %exp:cLoja%
    AND SE2.E2_EMISSAO >= %exp:DtoS(dDataIni)%
EndSQL

Nota: %exp:% trata quoting automaticamente para campos character. Para datas, usar DtoS() para converter.

%Order:TABLE%

Retorna a ordenacao de chave primaria para a tabela.

-- Input:
ORDER BY %Order:SE2%

-- Expande para as colunas de chave primaria de SE2

Declaracao de Tipo de Coluna

Declarar tipos de colunas do resultado para evitar chamadas manuais de TCSetField:

BeginSQL Alias cAlias
    column E2_EMISSAO as Date
    column E2_VENCTO  as Date
    column E2_VALOR   as Numeric(16,2)

    SELECT E2_PREFIXO, E2_NUM, E2_EMISSAO, E2_VENCTO, E2_VALOR
    FROM %table:SE2% SE2
    WHERE SE2.%notDel%
    AND SE2.E2_FILIAL = %xfilial:SE2%
EndSQL

// Colunas sao automaticamente tipadas - sem necessidade de TCSetField
DbSelectArea(cAlias)
While !Eof()
    Local dEmissao := (cAlias)->E2_EMISSAO   // Ja tipo Date
    Local nValor   := (cAlias)->E2_VALOR      // Ja Numeric
    DbSkip()
EndDo

Sem declaracao column, todos os campos retornam como Character e precisam de conversao manual.

Padroes de JOIN

INNER JOIN

BeginSQL Alias cAlias
    SELECT SE2.E2_PREFIXO, SE2.E2_NUM, SE2.E2_VALOR,
           SA2.A2_NOME, SA2.A2_CGC
    FROM %table:SE2% SE2
    INNER JOIN %table:SA2% SA2
        ON SE2.E2_FORNECE = SA2.A2_COD
        AND SE2.E2_LOJA   = SA2.A2_LOJA
        AND SA2.A2_FILIAL = %xfilial:SA2%
        AND SA2.%notDel%
    WHERE SE2.%notDel%
    AND SE2.E2_FILIAL = %xfilial:SE2%
    AND SE2.E2_EMISSAO >= %exp:DtoS(dDataIni)%
    ORDER BY SE2.E2_EMISSAO
EndSQL

LEFT JOIN

BeginSQL Alias cAlias
    SELECT SC5.C5_NUM, SC5.C5_CLIENTE, SC5.C5_LOJACLI,
           SA1.A1_NOME
    FROM %table:SC5% SC5
    LEFT JOIN %table:SA1% SA1
        ON SC5.C5_CLIENTE = SA1.A1_COD
        AND SC5.C5_LOJACLI = SA1.A1_LOJA
        AND SA1.A1_FILIAL = %xfilial:SA1%
        AND SA1.%notDel%
    WHERE SC5.%notDel%
    AND SC5.C5_FILIAL = %xfilial:SC5%
EndSQL

Boa pratica: Sempre usar sintaxe de JOIN explicito, nao FROM separado por virgula.

Padroes de Agregacao

SUM / COUNT / AVG

BeginSQL Alias cAlias
    column TOTAL as Numeric(16,2)
    column QTD   as Numeric(10,0)

    SELECT SE2.E2_FORNECE, SE2.E2_LOJA,
           SUM(SE2.E2_VALOR) AS TOTAL,
           COUNT(*) AS QTD
    FROM %table:SE2% SE2
    WHERE SE2.%notDel%
    AND SE2.E2_FILIAL = %xfilial:SE2%
    AND SE2.E2_EMISSAO BETWEEN %exp:DtoS(dDataIni)% AND %exp:DtoS(dDataFim)%
    GROUP BY SE2.E2_FORNECE, SE2.E2_LOJA
    HAVING SUM(SE2.E2_VALOR) > 0
    ORDER BY TOTAL DESC
EndSQL

Subquery

BeginSQL Alias cAlias
    SELECT SA1.A1_COD, SA1.A1_NOME
    FROM %table:SA1% SA1
    WHERE SA1.%notDel%
    AND SA1.A1_FILIAL = %xfilial:SA1%
    AND SA1.A1_COD IN (
        SELECT SC5.C5_CLIENTE
        FROM %table:SC5% SC5
        WHERE SC5.%notDel%
        AND SC5.C5_FILIAL = %xfilial:SC5%
        AND SC5.C5_EMISSAO >= %exp:DtoS(dDataIni)%
    )
EndSQL

Padrao Completo: Query com Iteracao de Cursor

#Include "TOTVS.CH"
#Include "TopConn.ch"

User Function QueryEx()
    Local cAlias := GetNextAlias()
    Local aArea  := GetArea()
    Local aResult := {}
    Local dDataIni := Date() - 30

    BeginSQL Alias cAlias
        column E2_EMISSAO as Date
        column E2_VENCTO  as Date
        column E2_VALOR   as Numeric(16,2)
        column SALDO      as Numeric(16,2)

        SELECT SE2.E2_PREFIXO, SE2.E2_NUM, SE2.E2_PARCELA,
               SE2.E2_FORNECE, SE2.E2_LOJA,
               SE2.E2_EMISSAO, SE2.E2_VENCTO,
               SE2.E2_VALOR,
               (SE2.E2_VALOR - SE2.E2_PAGO) AS SALDO,
               SA2.A2_NOME
        FROM %table:SE2% SE2
        INNER JOIN %table:SA2% SA2
            ON SE2.E2_FORNECE = SA2.A2_COD
            AND SE2.E2_LOJA   = SA2.A2_LOJA
            AND SA2.A2_FILIAL = %xfilial:SA2%
            AND SA2.%notDel%
        WHERE SE2.%notDel%
        AND SE2.E2_FILIAL = %xfilial:SE2%
        AND SE2.E2_EMISSAO >= %exp:DtoS(dDataIni)%
        AND (SE2.E2_VALOR - SE2.E2_PAGO) > 0
        ORDER BY SE2.E2_VENCTO
    EndSQL

    DbSelectArea(cAlias)
    While !(cAlias)->(Eof())
        aAdd(aResult, {;
            (cAlias)->E2_PREFIXO,;
            (cAlias)->E2_NUM,;
            (cAlias)->E2_VALOR,;
            (cAlias)->SALDO,;
            (cAlias)->A2_NOME;
        })
        (cAlias)->(DbSkip())
    EndDo
    (cAlias)->(DbCloseArea())

    RestArea(aArea)
Return aResult

Restricoes e Armadilhas

RegraPor que
NAO iniciar uma linha com * dentro de BeginSQLPre-compilador ADVPL trata * no inicio da linha como comentario
Sempre incluir %notDel%Protheus usa delecao logica, nunca confiar em fisica
Sempre incluir %xfilial% ou filtro de filialAmbientes multi-filial retornarao dados errados
Usar GetNextAlias() para o aliasEvita conflitos de nome de alias com work areas abertas
Sempre DbCloseArea() ao terminarPrevine vazamento de work areas (numero limitado de aliases)
NAO usar SELECT *Especificar colunas explicitamente para performance e clareza
Valores de data devem usar DtoS()Banco armazena datas como strings YYYYMMDD
Declaracoes column vao ANTES do SELECTDefinem tipos do resultado, nao colunas da query

Operacoes DML

Para INSERT, UPDATE, DELETE usar TCSqlExec em vez de BeginSQL:

// INSERT
Local cSql := "INSERT INTO " + RetSqlName("ZZ1") + " "
cSql += "(ZZ1_FILIAL, ZZ1_CODIGO, ZZ1_DESCRI, D_E_L_E_T_, R_E_C_N_O_) "
cSql += "VALUES ('" + xFilial("ZZ1") + "', '001', 'Teste', ' ', " + cValToChar(GetSxeNum("ZZ1","ZZ1_CODIGO")) + ")"
nRet := TCSqlExec(cSql)

// UPDATE
cSql := "UPDATE " + RetSqlName("ZZ1") + " SET "
cSql += "ZZ1_DESCRI = 'Novo Valor' "
cSql += "WHERE ZZ1_FILIAL = '" + xFilial("ZZ1") + "' "
cSql += "AND ZZ1_CODIGO = '001' "
cSql += "AND D_E_L_E_T_ = ' '"
nRet := TCSqlExec(cSql)

If nRet < 0
    Conout("SQL Error: " + TCSqlError())
EndIf

BeginSQL e somente para SELECT. Usar TCSqlExec para DML (INSERT/UPDATE/DELETE).

Dicas de Performance

  1. Usar declaracoes column para evitar conversoes de tipo pos-query
  2. Usar JOINs em vez de subqueries quando possivel
  3. Filtrar cedo com WHERE, nao apos ler todas as linhas em ADVPL
  4. Usar hint NOLOCK para queries somente leitura (SQL Server): FROM %table:SA1% SA1 WITH(NOLOCK)
  5. Usar TOP N quando precisar apenas de resultados limitados
  6. Fechar aliases imediatamente apos uso para liberar work areas
  7. Evitar loops com BeginSQL dentro - construir uma query que retorna todos os dados necessarios

Migracao de TCQuery para BeginSQL

Antes (concatenacao de strings):

Local cQuery := ""
cQuery += "SELECT A1_COD, A1_NOME "
cQuery += "FROM " + RetSqlName("SA1") + " SA1 "
cQuery += "WHERE SA1.D_E_L_E_T_ = ' ' "
cQuery += "AND A1_FILIAL = '" + xFilial("SA1") + "' "
cQuery += "AND A1_TIPO = '" + cTipo + "'"
TCQuery cQuery New Alias "QRY_CLI"

Depois (Embedded SQL):

Local cAlias := GetNextAlias()

BeginSQL Alias cAlias
    SELECT A1_COD, A1_NOME
    FROM %table:SA1% SA1
    WHERE SA1.%notDel%
    AND SA1.A1_FILIAL = %xfilial:SA1%
    AND SA1.A1_TIPO = %exp:cTipo%
EndSQL

Beneficios: Sem RetSqlName manual, sem D_E_L_E_T_, sem xFilial(), sem quoting de strings, sem risco de SQL injection.

Nesta pagina