FluentSQL
1.5.1Open SourceCRA-readyChangelog
Todas as mudanças notáveis deste projeto serão documentadas aqui.
O formato segue Keep a Changelog. Versionamento segue Semantic Versioning.
[Unreleased]
Removed
BREAKING CHANGE —REVERTIDO antes de sair em versão — ver Added abaixo. OTFluentSQLDriverperdeu 6 valores.cd9e71dtinha removidodbnInformix,dbnADS,dbnASA,dbnAbsoluteDB,dbnElevateDBedbnNexusDBdo enum público; os valores voltaram, na posição original. Nenhuma versão publicada chegou a sair sem eles, então não há BREAKING CHANGE a anunciar em nenhuma direção.- Removidos overrides mortos e inalcançáveis de
Round,FlooreAbsemFluentSQL.Functions{MSSQL,MySQL,PostgreSQL}.pas, e os overrides de agregação deFluentSQL.FunctionsMongoDB.pas. - Permanecem removidos — e não voltam com os 6 valores: as
_RegisterInformix/ADS/ASA/AbsoluteDB/ElevateDB/NexusDBdeFluentSQL.Register.pas, as entradas correspondentes nousescondicional (apontavam para ficheiros que nunca existiram no repositório) e os{$DEFINE}deles emFluentSQL.inc. O enum voltou; o registo, não.
Added
TFluentSQLDriverrecuperou os 6 valores, na posição e ordem originais:dbnInformix(ordinal 7),dbnADS(9),dbnASA(10),dbnAbsoluteDB(11),dbnElevateDB(13) edbnNexusDB(14) — o enum volta a ter 15 membros, edbnPostgreSQL(8) edbnMongoDB(12) voltam aos ordinais que tinham antes docd9e71d. A ordem foi restaurada, e não apenas a lista: o valor é serializável e consumidores externos nomeiam os membros.Por que voltaram: a remoção partiu do diagnóstico de que eram "drivers fantasma" — e a parte do diagnóstico sobre o FluentSQL continua verdadeira (não há unit deles em
Source\Drivers). O que o diagnóstico não previu é que consumidores nomeiam esses valores no próprio código:TDMLGeneratorAbstract.ResolveFluentSQLDriver, emJanus.DML.Generator.pas:641,643,645,656, devolvedbnADS,dbnAbsoluteDB,dbnElevateDBedbnNexusDB— e um identificador que deixa de existir no enum éE2003 Undeclared identifierem quem o nomeia (medido nesta árvore removendodbnADSde novo:FluentSQL.Utils.pas(382)e(417)— as linhas do rótulodbnADSnos doiscasede data; a versão anterior desta frase citava (383)/(418), que são as linhas dodbnASA). Restaurar a superfície é decisão do dono do projeto.O que NÃO voltou, e é o ponto: continua sem existir unit
FluentSQL.Serialize*/Select*/Functions*desses 6, sem_Register*e sem{$DEFINE}. Pedir deles algo que passe pelo registo levantaEFluentSQLDriverNotRegistered— a exceção nomeada que ocd9e71dcriou —, e nãoEAccessViolationnemEAbstractError. Medido emTest Delphi\Common_tests\test.driver.functions.matrix.pas: 6 dialetos × 4 portas de entrada (Register.Functions,Register.Select,Register.Serializee a chamada de função do padrão B), 24 células, todas com a exceção nomeada; e a varredura da matriz inteira passou de 9 × 26 = 234 células para 15 × 26 = 390, sem nenhuma falha não nomeada.Nem tudo levanta, e a afirmação acima é qualificada de propósito. Das 156 células dos 6 dialetos (26 funções cada), 90 levantam
EFluentSQLDriverNotRegistered— as 15 funções do padrão B, que consultam o driver — e 66 devolvem texto ANSI — as 11 do padrão A (Count,Sum,Min,Max,Average,Abs,Cast(String),Upper,Lower,Round,Floor), que o núcleo emite sem consultar o driver:Func(dbnADS).Count('C')devolveCOUNT(C), exatamente como devolvia antes docd9e71d. Nenhuma das 156 devolvenil,EAccessViolationouEAbstractError. O ponto cego do padrão A já estava descrito no cabeçalho do ficheiro de teste e não foi introduzido nem alargado aqui.Uma exceção diferente, catalogada e não tocada aqui:
TFluentSQLRegister.DDLSerializelevantaENotSupportedException(e nãoEFluentSQLDriverNotRegistered) para dialeto sem registo — comportamento anterior a esta mudança, igual paradbnInterbaseedbnDB2.FluentSQL.Utils.pasrecuperou os ramos de formatação de data dos 6, com a mesma formatação de antes docd9e71d:dbnAbsoluteDBcommm/dd/yyyy(junto de Firebird e Interbase) e os outros cinco comyyyy-mm-dd.
Changed
BREAKING CHANGE (SQL emitido e API) —
CaseExprpassou a perguntar se o nó do cursor é uma COLUNA antes de se ancorar nele.IFluentSQL.CaseExprdecide duas coisas: o operando doCASE(o que vem entreCASEe o primeiroWHEN) e em que nó da árvore oCASEvai morar. Ele decidia as duas lendoFAST.ASTName— um cursor que aponta para o último nó tocado pela cadeia fluente — sem perguntar que nó é aquele.O que NÃO mudou, e é a maior parte do uso: com o cursor sobre uma coluna,
CaseExprsem argumento continua herdando o nome dela e continua substituindo aquela coluna..Select.Column('ID').Column('TIPO').CaseExpr.When('1')…continua emitindoSELECT ID, (CASE TIPO WHEN 1 THEN … END) …, byte a byte. É um idioma deliberado e público — "transforme a última coluna numCASEsimples sobre ela" —, e é a forma dominante da suíte: medido por mutação, apagá-lo derruba 33 células nos 11 runners (26 emCommon, 11 da suíte do slot de valor) — e apagar o anexo derruba 41 / 30, porque sem ele oCASEnão chega aoSELECT. Toda contagem publicada foi remedida no HEAD final, com as duas bases: 11 runners eCommon, sempre com-DDB2 -DINTERBASE.CaseExpr('COLUNA')explícito também não muda.E a pergunta é de NÓ, não de seção — a distinção não é acadêmica, e é o motivo de a primeira versão desta entrega ter sido rejeitada.
FAST.ASTNameé durável e atravessa a troca de seção;FAST.ASTColumnsé trocado por baixo do cursor (viranilnoWHEREe noHAVING, vira outra lista noGROUP BYe noORDER BY). Duas cadeias com as mesmas seções e ordens diferentes são caminhos distintos:Cadeia cursor cai em antes depois .Select.From('T').Column('TIPO').Where(…)coluna SELECT (CASE TIPO …) FROM T WHERE …inalterado .Select.Column('TIPO').From('T').Where(…)relação SELECT TIPO FROM (CASE T …) WHERE …EArgumentExceptionO que mudou é o que acontecia com o cursor em qualquer nó que não é coluna:
Cursor em / seção Antes Depois relação do FROM, seçãoSELECTSELECT * FROM (CASE PRODUCTS WHEN PRICE > 10 …)— recusado pelos 7SELECT *, (CASE WHEN PRICE > 10 …) FROM PRODUCTS— ⚠️ 2 dos 6 dialetos ATIVOS (Firebird e Oracle) recusam este texto, pela vírgula depois da estrela; ver a nota doAllabaixorelação, seção GROUP BYSELECT TIPO FROM (CASE T …)SELECT TIPO FROM T GROUP BY (CASE …)relação, seção ORDER BYSELECT TIPO FROM (CASE T …)SELECT TIPO FROM T ORDER BY (CASE …) ASCrelação do JOIN… INNER JOIN (CASE U …) ONEArgumentExceptionseção WHERE/HAVINGSELECT … FROM (CASE T …) WHERE …EArgumentExceptionseção DELETE, comFrom, semFrom, e com qualquer cláusula intercaladaDELETE FROM (CASE T …)inválido — ouEAccessViolationsemFromEArgumentExceptionseção UPDATE, com ou sem seção intercaladaEAccessViolationEArgumentExceptionInsert.Into('T')EAccessViolationEArgumentExceptionInsert.Into('T').Column('A')INSERT INTO T ( (CASE A …) )EArgumentExceptionnil(Selectsem coluna, ou nemSelect)EAccessViolationemite, ou recusa nomeada Não é só SQL inválido — é perda de ESTRUTURA.
TFluentSQLName.SerializeprefereFCaseaFName, então escrever oCASEnum nó de relação apaga o texto daquele nó: oFROMsome, ou a tabela juntada some e oONfica órfão.Sobre o
EAccessViolation: não era guarda faltando, era guarda no objeto errado. O código tinhaif Assigned(FAST)— masFASTnunca énilali. Quem éniléFAST.ASTName, desreferenciado duas linhas acima da guarda e dentro dela.A coluna nova vai para
ASTColumns, a lista da seção corrente — e não para a projeção. ForçarSelect.Columnsfoi medido e quebraria dois casos legítimos: quem chamouOrderBy('')quer oCASEnoORDER BY, e quem chamouGroupBy('')quer noGROUP BY. O destino já estava certo; o erro estava no predicado.O
INSERTrecusa por razão própria: as colunas doINSERTsão nomes de destino — as células onde o dado será gravado — e não expressões projetadas; umCASEnão pode ser alvo de gravação em dialeto nenhum. Metade disso não é regressão desta entrega:Insert.Into('T').Column('A')já emitiaINSERT INTO T ( (CASE A …) )calado na base.A regra que decide todos os casos: converter SQL inválido silencioso em SQL válido quando o sentido é inequívoco, e em erro nomeado quando não é. Nunca em descarte silencioso.
O oráculo de motor: 7 submetidos — 6 ativos (PostgreSQL, MySQL, SQL Server, Firebird, Oracle, SQLite) e 1 sob define (DB2, desligado no
FluentSQL.inc). Todos os enunciados foram submetidos como o FluentSQL os emite — sem apelido acrescentado, semAS, sem reordenar; onde o motor recusa, a recusa está transcrita. Massa de 3 linhas, contagem 3 antes e 3 depois.Motor texto antigo projeção sem estrela projeção com estrela ORDER BYGROUP BYPostgreSQL 16.14 syntax error at or near "CASE"3 linhas ✔ 3 linhas ✔ 3 ✔ 42803MySQL 8.4.11 ERROR 1064 (42000)3 ✔ 3 ✔ 3 ✔ ERROR 1055SQL Server 16.0.4265.3 Msg 1563 ✔ 3 ✔ 3 ✔ Msg 8120Firebird 5.0.4 -104 Token unknown - CASE3 ✔ -104na vírgula3 ✔ -104Oracle AI 26ai 23.26.2.0.0 ORA-009073 ✔ ORA-009233 ✔ ORA-00979DB2 v12.1.5.0 SQL0104N3 ✔ 3 ✔ 3 ✔ SQL0119NSQLite 3.53.4 Parse error near "CASE"3 ✔ 3 ✔ 3 ✔ 2 linhas O texto antigo é recusado por SETE de sete. Mas a forma nova não passa em todos, e esta entrada não afirma que passa:
- projeção sem estrela e
ORDER BY: 7 de 7 aceitam, com o dado certo; - projeção com estrela: 5 de 7 submetidos — e, na unidade que importa para decidir, 4 dos 6 ATIVOS. Firebird e Oracle recusam, e são dois dos seis dialetos ativos, não dois de sete. A causa não é a ancoragem, é a vírgula depois da estrela: o Firebird aponta a coluna 9 (a vírgula) e a Oracle devolve
ORA-00923.SELECT *, <expr>já saía da base porSelect.Allseguido deColumn; é defeito pré-existente doAll, com porta própria, fora do escopo desta tarefa. O que mudou foi trocar um texto que 7 de 7 recusam por um que 5 de 7 aceitam, e nos outros 2 a recusa passou a ser de outra causa, já catalogada. GROUP BY: 1 de 7. Os seis recusam porque projetar uma coluna agrupando por outra expressão viola a regra deGROUP BY— causa da cadeia do usuário, não da ancoragem. O SQLite aceita por não aplicar a regra estrita.
Quem é atingido: quem chamava
CaseExprcom o cursor fora de uma coluna — ou seja, quem já produzia SQL que motor nenhum aceitava, ou já estourava. As cadeias que emitiam SQL válido continuam emitindo o mesmo texto. O que fazer: chameCaseExprcom uma coluna corrente (Column(...)antes), ou numa seção que projete (Select,GroupBy,OrderBy). Transcrição literal, com massa e contagem antes/depois, emTest Delphi\Common_tests\test.caseexpr.anchor.matrix.sql. InterBase não foi medido — não existe imagem pública, e não foi inferido do Firebird.CASEfora de umSELECTque projeta passou a ser recusado, e a guarda faz QUATRO perguntas — desenhadas a partir de uma VARREDURA, não de uma lista. As versões anteriores desta entrega tinham guarda paraDELETE/UPDATE, outra para oINSERTpor comparação de coleção, e uma genérica — e três rodadas de revisão seguidas acharam o irmão de um passo adiante do que a rodada anterior fechara. A causa era o método: enumerar caso a caso para onde a lista acaba. Esta versão saiu de um produto cartesiano de 300 cadeias (espécie × posição do alvo × cláusula intercalada × forma da chamada), com o critério "o texto emitido É um enunciado?" — e a varredura devolveu 18 cadeias que a base respondia comEAccessViolatione o HEAD emitia calado, em três famílias, das quais só uma tinha sido nomeada por revisão:família exemplo base antes da correção INSERTcom cláusula intercaladaInsert.Into('T').GroupBy('')EAccessViolationINSERT INTO T GROUP BY (CASE …)INSERTsemIntoInsert.GroupBy('')EAccessViolationGROUP BY (CASE …)SELECTsem projeção (não é DML)Select.GroupBy('')EAccessViolationGROUP BY (CASE …)As quatro perguntas, cada uma com célula própria e partição medida: (1) há enunciado aberto? (2) a espécie é
SELECT? — uma resposta paraINSERT,UPDATEeDELETE, porque é a mesma; (3) há lista de colunas? (4) se a lista é acessória (GROUP BY/ORDER BY), existe projeção que ela qualifique? A (4) vale nos dois caminhos — âncora e coluna nova —, e foi a varredura que mostrou por quê:Select.GroupBy('').Column('K')põe a coluna dentro doGROUP BY, o cursor ancora nela, e não havia coluna nova a criar. Após a correção: 0 defeitos em 300 cadeias — pelo critério da varredura, que é "o texto emitido é um enunciado?". O zero é qualificado: ele não cobre oFROMpendurado, que é pré-existente e independente doCaseExpr(ver Catalogado e NÃO consertado).A partição real das quatro perguntas, medida em
Common, com o sítio que cada número mutou:pergunta sítio mutado células que caem (1) há enunciado aberto? corpo da guarda 1 (2) a espécie é SELECT?corpo da guarda 4 (3) há lista de colunas? corpo da guarda 7 (4) a acessória tem projeção? corpo da guarda 5 só o sítio da coluna nova 4 só o sítio da âncora 1 Os dois sítios acima são disjuntos e somam o corpo (4 + 1 = 5); publicar só o "4" deixaria de fora
TestColunaCaindoDentroDoGroupBySemProjecaoRecusa, a única que o sítio de âncora derruba.A (4) é chamada de TRÊS sítios, e o terceiro não aparece na tabela porque responde a outro invariante. São
FluentSQL.pas:505(âncora, sobrecargaString),:673(coluna nova) e:732(âncora, sobrecargaarray of const). Mutados isoladamente,505derruba 1 célula e732derruba 0 — e isso não é sítio esquecido::732existe pelo invariante de vazamento de parâmetro documentado emFluentSQL.pas:707-727(a recusa tem de correr antes deSqlArrayOfConstToParameterizedSqlgravar os:pN), e quem o expõe é a mutação (3), não a (4). O 1 publicado para o sítio de âncora vem de505e é robusto.⚠️ O número
772/718sai com o símbolo GLOBAL do compilador, não pelo.inc—dcc32 -DDB2;INTERBASE. LigarINTERBASE/DB2emSource\FluentSQL.incdevolve770/716/19/35: as 2 células a mais são{$IFDEF DB2}e{$IFDEF INTERBASE}emtest.cases.value.pas:231e:235, e essa unit não inclui o.inc. Numa entrega cuja tese é medição reproduzível, o caminho que se tentaria primeiro dá outro número — então o comando fica dito junto do número.A suíte NÃO é verde, e
11/11 runnerse0 leakednão devem ser lidos como tal: são 46 células vermelhas sem defines e 35 com eles, todas pré-existentes — ocommvazio nos dois sentidos prova que nenhuma é desta entrega. Distribuição medida: config commitada — DB2 23, InterBase 15, Oracle 8; config all-drivers — DB2 13, Firebird 9, Oracle 8, InterBase 5.A não-redundância da (1) é DE MENSAGEM, não de aceitar/recusar — e a distinção é medida: apagando-a, a cadeia continua recusada e com a mesma classe, porque a (4) responde no lugar. O que muda é a prescrição, e a alternativa realmente falha:
Query(d).GroupBy('').Column('K')+CaseExprcontinua levantando, logo quem recebesse "chameColumn(...)" naquele estado seguiria a prescrição e falharia de novo.ESTA ENTREGA SUBSTITUIU a forma do PR #167, não a estendeu. Uma versão intermediária desta branch usava a forma de lá — marca durável (
not FAST.Delete.IsEmpty) mais seção ativa (FActiveSection) — e ela não embarca: no HEAD,FAST.Delete.IsEmptyaparece duas vezes em todo oSource\** —FluentSQL.pas:1248, em código, e:1232, no comentário da mesma rotina —, **ambas dentro do_CreateJoin**, que é a guarda do #167 e não faz parte desta entrega. **No caminho doCaseExprnão há marca durável nem leitura deFActiveSection: quem responde éFStatementKind.** O motivo da troca é medido: a forma do #167 pergunta sobre **verbos DML**, e **Select.GroupBy('')não é DML — ela não alcançaria a terceira família de defeitos, que é justamente a que nenhuma revisão tinha nomeado.Sem as perguntas (2) e (4), esta entrega introduziria
loud→mute— e duas dessas linhas seriam regressão nossa, não herdada:cadeia base d867cadsem a guarda com a guarda Delete.From('T').GroupBy('')DELETE FROM (CASE T …)inválidoDELETE FROM T GROUP BY (CASE …)caladorecusa Delete.From('T').OrderBy('')DELETE FROM (CASE T …)inválidoDELETE FROM T ORDER BY (CASE …) ASCcaladorecusa Update('T').Values(…).GroupBy('')EAccessViolationUPDATE T SET A = :p1 GROUP BY (CASE …)caladorecusa Update('T').Values(…).OrderBy('')EAccessViolation… ORDER BY (CASE …) ASCcaladorecusa As duas naturezas são diferentes e o texto as distingue: no
DELETEa base já emitia texto inválido, então é inválido → recusado, melhora sem regressão; noUPDATEa base estourava, e sem a guarda a entrega trocaria crash por texto inválido calado — exatamente a regressão que esta tarefa existe para não cometer. Continua liberado trocar paraSELECTna mesma instância, que reabre a espécie e devolve oCaseExpr(duas células travam isso).⚠️ As quatro linhas da tabela acima são cobertas EM DOBRO por (2) e (4), e por isso nenhuma mutação individual as derruba — medido,
q1=q2=q3=q4devolvem zero para elas; caem sob o par (2)+(4). Com a espécie neutralizada, a pergunta da acessória responde no lugar, porqueSelect.Columnsestá vazia. Isso não enfraquece a correção — as células estão verdes —, mas cobertura em dobro não é partição, e a diferença só aparece rodando.Catalogado e NÃO consertado, dois itens, os dois pré-existentes e medidos nas duas árvores:
- Dois
CaseExprseguidos sobre a mesma coluna — o segundo descarta o primeiro em silêncio. Consequência direta do idioma "substitui a última coluna". FROMpendurado numSELECTsem relação —.Select.Column('K')+CaseExpr, semFrom, emiteSELECT (CASE K …) FROM. É independente doCaseExpr:Select.Column('K').AsStringjá devolveSELECT K FROMnas duas árvores. É defeito do serializador doFROMvazio, com porta própria. Esta entrega não o cria nem o piora — apenas passou a alcançá-lo em cadeias que antes estouravam antes de chegar lá.
- projeção sem estrela e
BREAKING CHANGE (API) —
IFluentSQL.CaseExpr(const AExpression: IFluentSQLCriteriaExpression)passou a levantarEArgumentExceptionnomeada. A sobrecarga era pública e 100% inalcançável: o corpo faziaTFluentSQLCriteriaCase.Create(Self, '')seguido deResult.AndOpe(AExpression), eAndOpelêFLastExpression, que só é preenchido porWhen. Recém-criado o campo énil, então a chamada estourava comEAccessViolationem qualquer estado — medido em três. Não havia um único teste que a exercitasse.Ela não foi "consertada para funcionar", e a razão é medida. A saída óbvia — serializar a expressão e delegar à sobrecarga de
String— funciona quando a expressão pertence ao mesmo enunciado e MENTE quando não pertence:QA.CaseExpr(QB.Expression(['TIPO', '*', 2])) SQL de QA: SELECT (CASE TIPO * :p1 WHEN …) FROM T QA.Params.Count = 0 <- o :p1 citado NÃO EXISTE na coleção de QA QB.Params.Count = 1 <- o valor ficou na coleção do outroO enunciado sairia citando um parâmetro fantasma, e quem liga por posição ligaria errado sem erro nenhum — a mesma classe de dano silencioso que esta entrega combate. E não há como distinguir os dois casos em runtime:
IFluentSQLCriteriaExpressionexpõeAsStringeExpression, e nada mais; descobrir o dono exigiria alargar a interface, que é exatamente o pré-requisito já catalogado como fusão de coleções de parâmetro (PR #166).Entre estourar, mentir em silêncio e recusar dizendo o porquê, a recusa é a única honesta. Quando a fusão existir, a recusa vira aditiva de remover. Quem é atingido: ninguém com código que funcionasse — o comportamento anterior era
EAccessViolation. O que fazer: useCaseExpr(const AExpression: String)com o termo, ou a sobrecarga dearray of const, que parametriza na coleção certa.BREAKING CHANGE (API) —
Schema(dbnMySQL).TruncateTable([…])com mais de uma tabela deixou de emitir SQL e passou a levantarENotSupportedException. Antes saíaTRUNCATE TABLE `T1`, `T2`, e duas células verdes fixavam esse texto (test_esp074_unit.pasetest.ddl.mysql.pas). O MySQL recusa: medido emmysql:8.4(sha256:b3b90af2…fd3fb,SELECT VERSION()= 8.4.11), o enunciado devolveERROR 1064 (42000) … near ',T2'. Controle positivo na mesma sessão:TRUNCATE TABLE `T1`executou e esvaziou a tabela (3 → 0).Isto não é convenção nova — é a convenção que o próprio framework já aplicava a todos menos ao MySQL.
TFluentDDLSerializerMSSQL.TruncateTable,…Oracle.TruncateTable,…SQLite.TruncateTable,…Firebird.TruncateTableeTFluentDDLSerializeAbstract.TruncateTablejá levantavamENotSupportedExceptionnesta mesma construção desde o ESP-074. A lista de tabelas num únicoTRUNCATEé exclusividade do PostgreSQL entre os 6 relacionais ativos, e isso foi medido nos seis: PostgreSQL 16.14 executa (a1: 2 → 0,a2: 2 → 0); SQL Server 2022 devolveMsg 102 … Incorrect syntax near ','; Oracle 23.26 devolveORA-03291: Invalid truncate option - missing STORAGE keyword; SQLite e Firebird não têmTRUNCATE.Não há forma equivalente numa instrução, e "emita dois
TRUNCATE" não é equivalente: no MySQL oTRUNCATEfaz commit implícito — medido: dentro deSTART TRANSACTION, umINSERTseguido deTRUNCATEeROLLBACKdeixou a tabela vazia — logo duas instruções são duas transações, enquanto o multi-tabela do PostgreSQL é atômico. O contrato declarado do builder é one command perAsString(ESP-029).O que fazer: emitir uma chamada por tabela. Quem comparava o SQL gerado com string fixa troca a expectativa por
Assert.WillRaise(…, ENotSupportedException). Efeito colateral declarado e medido:TruncateTable([…]).Partition(…)continua levantandoENotSupportedException, mas agora é a guarda de multi-tabela que a atende — a dePARTITIONnão é mais alcançada por ela. Provado por mutação, não argumentado: remover a guarda de multi-tabela mata aquela célula; mexer no ramo dePARTITIONnão a toca. Ela virou uma segunda medição da guarda de multi-tabela, e isso está escrito na própria célula. A classe não mudou, e nenhuma célula assere a mensagem.BREAKING CHANGE (SQL emitido) —
Schema(dbnMySQL).TruncateTable(t).Partition(p)deixou de emitirTRUNCATE TABLEe passou a emitirALTER TABLE.texto emitido Antes TRUNCATE TABLE `logs` PARTITION (p2023)Depois ALTER TABLE `logs` TRUNCATE PARTITION `p2023`O texto anterior é sintaxe Oracle e o MySQL o recusa: medido em
mysql:8.4(sha256:b3b90af2…fd3fb,SELECT VERSION()= 8.4.11), devolveERROR 1064 (42000) … near 'PARTITION (p2023)'. Não é superfície nova: a API já ofereciaTRUNCATE … PARTITIONe já emitia texto que o motor recusa — fazê-la emitir a forma válida é conserto.ALTER TABLE … TRUNCATE PARTITIONnão é substituto aproximado: é a única forma que o MySQL tem para a operação — não existeTRUNCATE TABLE … PARTITIONno dialeto para comparar contra ela, logo não há desvio de semântica a declarar. Aceita com e sem crase no nome da partição; as duas foram submetidas.Verificação ponta a ponta, com massa em duas partições para que "esvaziou a certa" seja verificável: três linhas, duas em
p2023e uma empmax; submetido verbatim o texto que o HEAD emite —p20232 → 0,pmax1 → 1, e oSELECTfinal devolveu exatamente a linha de 2025. Controle negativo: o texto anterior devolveERROR 1064na mesma sessão.Nenhum dos outros cinco relacionais emite texto inválido nesta chamada — medido no HEAD final,
.TruncateTable('logs').Partition('p2023')levantaENotSupportedExceptionem PostgreSQL, Firebird, SQL Server, Oracle e SQLite. Fica declarado que o Oracle tem a construção (ALTER TABLE t TRUNCATE PARTITION p) e ainda assim recusa: é lacuna de capacidade, não texto inválido, e preenchê-la é decisão de produto, catalogada e fora desta entrega.Essa enumeração é dos cinco relacionais e NÃO é exaustiva — falta o sexto dialeto ativo, e a omissão fica declarada:
Schema(dbnMongoDB).TruncateTable('c').Partition('p2023')descarta o modificador em silêncio, devolvendo{"delete":"c","deletes":[{"q":{},"limit":0}]}— byte a byte igual à chamada semPartition. Quem pede para esvaziar uma partição recebe um comando que apaga a coleção inteira. O texto é JSON válido, e é por isso que a frase "não emite texto inválido" não o alcançava. Não foi corrigido nem ganhou célula, de propósito: é pré-existente, o MongoDB está fora da promessa relacional, e célula ali congelaria um descarte silencioso destrutivo.Esta mudança criou um contrato — "partição é só do MySQL" — e ele passou a ser medido. Antes dela o modificador não produzia SQL válido em dialeto nenhum, e o que os outros cinco faziam era indiferente; nenhuma célula travava isso, e a lacuna foi provada por mutação: fazer o PostgreSQL emitir
' PARTITION (…)'em vez de levantar passava pela suíte inteira sem derrubar nada. Foram acrescentadas cinco célulasTestTruncateTable_<dialeto>_Partition_RaisesNotSupportedemtest_esp074_unit.pas— PostgreSQL, Firebird, SQL Server, Oracle e SQLite — cada uma medida load-bearing pela mutação que faz o seu dialeto emitir em vez de levantar, e cada mutante matando exatamente a sua célula e nenhuma outra. Nenhuma célula foi desligada para isso.Contagem consolidada das três entradas de
TRUNCATEacima, para que o delta não precise ser somado de cabeça: contra a base,Found696 → 701 ePassed630 → 636 —+6, e não+5: as cinco guardas novas mais a célula religada.Ignored20 → 19 (a religada),Failed+Errored46 → 46 e vermelhos idênticos nome a nome. Com-DDB2 -DINTERBASE:Found698 → 703,Passed643 → 649,Ignored20 → 19,Failed+Errored35 → 35.O que fazer: quem comparava o SQL gerado com string fixa atualiza a expectativa. A célula que fixava o texto certo já existia na suíte, desligada —
test.ddl.mysql.pas,[Ignore]da T6 dizendo "emite sintaxe Oracle …; MySQL exige ALTER TABLE t TRUNCATE PARTITION p" — e foi religada sem uma vírgula de ajuste, porque o texto que ela já assertava casa com o que o HEAD passou a emitir. Isso move os números:Ignored20 → 19,Passed630 → 631.BREAKING CHANGE (API) —
Schema(dbnFirebird).Table(…).Rename(…)deixou de emitir SQL e passou a levantarENotSupportedException. Antes saíaALTER TABLE "TAB_A" TO "TAB_B", fixado verde emtest.ddl.firebird.pas. O Firebird não tem DDL de renomear tabela — nenhuma forma, não apenas esta. Medido emfirebirdsql/firebird:5.0.4(sha256:85d0f9bf…d040d,ENGINE_VERSIONperguntado ao motor = 5.0.4), as três candidatas:Enunciado submetido Resposta do motor ALTER TABLE "TAB_A" TO "TAB_B"-104 -Token unknown - line 1, column 21 -TOALTER TABLE "TAB_A" RENAME TO "TAB_B"-104 -Token unknown - line 1, column 21 -RENAMERENAME TABLE "TAB_A" TO "TAB_B"-104 -Token unknown - line 1, column 1 -RENAMEControle positivo na mesma sessão: o vizinho que renomeia coluna —
ALTER TABLE "TAB_A" ALTER "LEGADO" TO "NOVO_NOME", que o driver também emite — executou,RDB$RELATION_FIELDSpassou a listarNOVO_NOMEe o dado da coluna sobreviveu. Esse não foi tocado.Fora do Firebird nada muda:
AlterTableRenameTabledo serializador base continua emitindoALTER TABLE … RENAME TO …para MySQL, PostgreSQL e SQLite, e as três células que fixam isso continuam verdes — provado por mutação dirigida no serializador base, que mata exatamente essas três. Dos 6, o Firebird é o único sem nenhuma forma; o SQL Server também recusa oALTER TABLE(Msg 102 … near 'RENAME') mas resolve porsp_rename, que executou.O que fazer: no Firebird, recriar a tabela e migrar as linhas na aplicação — não é uma instrução. Recusar segue a convenção que
FluentSQL.DDL.Serialize.Firebird.pasjá aplica ao que o dialeto não expressa (DropTablecomIfExists,CreateSchema,DropSchema, sequências).BREAKING CHANGE (API) —
Schema(dbnFirebird).DropIndex(…).IfExistsdeixou de emitir SQL e passou a levantarENotSupportedException. Antes saíaDROP INDEX IF EXISTS "IX_CLI_NOME", fixado verde emtest.ddl.firebird.pas. Medido emfirebirdsql/firebird:5.0.4com índice existente e índice ausente — o mesmo erro nos dois, o que prova que a recusa é de sintaxe e não de alvo:Enunciado submetido Resposta do motor DROP INDEX IF EXISTS "IX_CLI_NOME"(índice existia)-104 -Token unknown - line 1, column 15 -EXISTSDROP INDEX IF EXISTS "IX_NAO_EXISTE"(índice não existia)-104 -Token unknown - line 1, column 15 -EXISTSControle positivo:
DROP INDEX "IX_TMP2"— a forma nua, que o HEAD continua emitindo — executou e o índice sumiu deRDB$INDICES(1 → 0). Controle negativo: sobre índice ausente a forma nua devolve-607 … -Index not found, que é o que a aplicação passa a ter de tratar no lugar doIF EXISTS.Isto é incoerência interna corrigida, não decisão de produto: o método
DropTabledo mesmo arquivo já recusava o mesmo modificador no mesmo dialeto. Dos 6, PostgreSQL, SQL Server, SQLite e Oracle aceitamDROP INDEX IF EXISTS(medido nos quatro); o MySQL também recusa (ERROR 1064 … near 'IF EXISTS …') e o serializador MySQL já levantava por isso; o Firebird recusava no motor e agora recusa no builder.O que fazer: chamar
DropIndex(…)semIfExistse tratar-607na aplicação.Matriz completa das sete construções apontadas pela varredura do PR #163 — com digest de imagem, versão perguntada ao motor, transcrição literal, controle positivo e negativo, verificação ponta a ponta do texto que o HEAD emite e tabela de mutação dirigida — em
Test Delphi\Common_tests\test.dialect.unsupported.matrix.sql. Quatro das sete foram corrigidas; três ficaram medidas e declaradas, sem conserto, por convergirem numa mesma pergunta de convenção que subiu ao dono:TRUNCATE TABLEdo Firebird (há duas convenções em conflito dentro do próprio repositório — o SQLite emiteDELETE FROM, com célula verde; todo o resto levanta — e a semântica difere de forma medida: o identity seguiu em 3 e não em 1, eROLLBACKdesfez), oINSERTem lote no Firebird (feature publicada em[1.0.9]que não funciona num dialeto ativo; há duas formas de lote que o motor aceita, ambas medidas ponta a ponta, mas ambas exigem o tipo SQL de cada coluna, que o construtor não tem) e oINTERSECTno Firebird (retirá-lo tira da API algo que oROADMAPlista como entregue). InterBase não foi medido — não existe imagem pública.BREAKING CHANGE (API) —
JOINdentro de umDELETEdeixou de emitir SQL e passou a levantarEFluentSQLConstructNotSupported. Atinge os quatro tipos:Delete.From(…).InnerJoin(…),.LeftJoin(…),.RightJoin(…)e.FullJoin(…), com ou sem apelido, com ou semOnCond, e em qualquer ordem da cadeia fluente — inclusive comWhere,OrderByouGroupByintercalados entre oFrome o join.A guarda mora em
TFluentSQL._CreateJoine pergunta "este statement é umDELETE?", lendo a marca durável no AST (FAST.Delete, que_DefineSectionDeleteestabelece eClearAlllimpa) — não o cursor de seção, queWhere('')desloca sem emitir uma letra. Trocar paraSELECT,INSERTouUPDATEna mesma instância limpa a marca e libera o join de novo, e isso é intencional.A razão NÃO é "o texto emitido não executa". Essa é verdadeira e é a menor das duas. A que decide é o dano silencioso, medido em motor real com contagem antes/depois — massa de 4 linhas em
A, das quais 2 casam comB:Chamada Forma nativa correspondente Resultado medido From('A','X').LeftJoin('B','Y').OnCond('Y.AID = X.ID'), semWhereDELETE X FROM A AS X LEFT JOIN B AS Y ON Y.AID = X.IDA: 4 → 0,B: 2 → 2 em dois dos sete motores — executam, reportam sucesso e apagam a tabela inteiraa mesma chamada, forma portável DELETE FROM A AS X WHERE EXISTS (SELECT 1 FROM B AS Y WHERE Y.AID = X.ID)A: 4 → 2,B: 2 → 2 nos seteNa junção externa a condição é decorativa: não filtra nada, porque a junção preserva toda linha da relação da esquerda. Quem escreveu aquilo achava estar filtrando, e perde a tabela sem uma linha de erro. É a mesma classe do achado do Oracle no PR #160 — apagar mais do que se pediu, sem erro; lá era a relação errada, aqui é a relação certa por inteiro.
Por que a família inteira e não só o
LeftJoin: dentro doDELETEos membros não significam a mesma coisa — oInnerJoinfiltra (a forma nativa apaga 2 das 4), oLeftJoinnão (apaga 4 das 4). Liberar só oInnerJoincriaria uma distinção que a superfície fluente não insinua e que gramática nenhuma dos sete espelha: 5 dos 7 recusam os dois por parse, 2 dos 7 aceitam os dois.O que se perde, dito sem maquiagem: para o
InnerJoinisolado existe forma portável, medida e aceita pelos sete — a interseção não é vazia, e esta entrada não finge que seja. Entregá-la é tarefa própria, já catalogada, com o custo medido: oWHEREprecisa migrar para dentro da subconsulta, porque pode citar a relação juntada (Where('Y.Status').Equal(1)é alcançável e emiteWHERE (Y.Status = ?)).O
ONpendurado morre junto, e por cima. SemOnCondo texto saía… INNER JOIN B AS Y ON, comONe sem predicado — não é produto cartesiano, é sentença truncada, recusada pelos sete (o DB2 é o que nomeia melhor: espera<boolean_predicate>). A causa continua de pé e é de outra tarefa:TFluentSQLJoins.Serializeconcatena'ON'incondicionalmente eTUtils.Concatdescarta a condição vazia, o que produz o mesmoONpendurado também noSELECT.Quem é atingido: quem chamava
JOINnumDELETEe nunca executou o resultado (5 dos 7 dialetos nunca analisaram o texto); e — este importa — quem usadbnMSSQLoudbnMySQLcom apelido, onde o SQL executava. Se eraInnerJoin, o comportamento era o esperado e a substituição abaixo o reproduz; se eraLeftJoin, o que executava apagava a tabela inteira. O que fazer:Delete.From('A','X').Where('').Exists('SELECT 1 FROM B AS Y WHERE Y.AID = X.ID'), que sai verbatim e é aceita pelos sete. Se o que se queria era apagar de duas relações, são duas instruções.Matriz completa, com digest de imagem, versão perguntada ao motor, transcrição literal e controle positivo e negativo em
Test Delphi\Common_tests\test.delete.join.matrix.sql. InterBase não foi medido — não existe imagem pública.EFluentSQLConstructNotSupportedganhou uma sobrecarga de construtorCreate(AConstruct, AMedido, ASaida)— aditiva, o construtor de dois argumentos continua válido e nenhuma chamada existente muda. Ela existe por veracidade, não por estilo: a forma de dois argumentos afirma que a construção "não tem forma válida em NENHUM dos dialetos", o que é verdade para oDELETEmulti-relação (a união é vazia) e é falso para oJOINemDELETE, onde dois dos sete aceitam o texto — e é justamente por aceitá-lo que a construção é perigosa. Usar a forma antiga ali embarcaria na mensagem uma generalização que a própria medição desmente.BREAKING CHANGE (SQL emitido) —
Exists/NotExistsdeixaram de parametrizar a subconsulta e passaram a emiti-la verbatim.IFluentSQL.Exists(const ASubQuery: String)tratava o argumento como valor e o mandava para a coleção de parâmetros. O texto da subconsulta virava valor de bind:Dialeto Antes Depois dbnMSSQLDELETE X FROM A AS X WHERE (exists :p1)DELETE X FROM A AS X WHERE (exists (SELECT 1 FROM B AS Y WHERE Y.AID = X.ID))dbnMySQLDELETE FROM A AS X WHERE (exists ?)DELETE FROM A AS X WHERE (exists (…))dbnOracleDELETE FROM A X WHERE (exists :p1)DELETE FROM A X WHERE (exists (…))demais 4 … WHERE (exists :p1)… WHERE (exists (…))Com
p1 = 'SELECT 1 FROM B AS Y WHERE Y.AID = X.ID'— ou seja, o que chegaria ao motor seria a subconsulta como string, não como consulta.Params.Countcai de 1 para 0 nessas chamadas: quem itera os parâmetros do enunciado verá um a menos. Quem compara o SQL gerado com string fixa precisa atualizar as expectativas.Não é "a interseção é vazia": é a UNIÃO que é vazia — não há um motor sequer em que
(exists :p1)executasse. Medido em motor real, com controle negativo e positivo, transcrição literal,docker run, digest da imagem e versão perguntada a cada motor emTest Delphi\Common_tests\test.exists.subquery.sql:Dialeto (exists :p1)— o que a base emitia(exists '…')— o bind resolvido(exists (SELECT …))— o HEADPostgreSQL 16.14 syntax error at or near "$1"syntax errorDELETE 3, sobrevivem 4 e 5MySQL 8.4.11 ERROR 1064ERROR 1064sobrevivem 4 e 5 SQL Server 2022 (16.0.4265.3) Msg 102Msg 102(3 rows affected), sobrevivem 4 e 5Firebird 5.0.4 SQL error code = -104-104sobrevivem 4 e 5 SQLite 3.53.4 near ":p1": syntax errorsyntax errorsobrevivem 4 e 5 Oracle 23.26.2.0.0 ORA-00906ORA-009063 rows deleted, sobrevivem 4 e 5DB2 12.1.5.0 SQL0104NSQL0104Nsobrevivem 4 e 5 InterBase não medido — não existe imagem pública não medido não medido A verificação não parou no parser. Em cada motor foram criadas
A(5 linhas) eB(3 linhas apontando para 1, 2 e 3), contada a massa antes, submetido verbatim o enunciado que o HEAD emite, e conferido que sobraram exatamente as linhas 4 e 5 (Exists) e 1, 2 e 3 (NotExists). Na Oracle, onde o SQL*Plus intercepta:p1no cliente, o negativo foi refeito comEXECUTE IMMEDIATE … USINGpara que o servidor parseasse com o bind carregando a subconsulta.Por que isto era grave: o
CHANGELOGe a mensagem deEFluentSQLConstructNotSupporteddeDELETEmulti-relação publicam que a saída é "restrinja a única relação alvo peloWHERE— inclusive com subconsulta, que é portável nos sete". Essa saída não existia: pela porta natural (Exists) ela emitia SQL que nenhum motor executa. O framework documentava um caminho que não abria.Onde a regra mora, e por que não houve máquina nova. Em
TFluentSQLOperator.GetCompareValue(FluentSQL.Operators.pas), numa guarda de tipo no topo do método:if FDataType = dftText then Result := '(' + VarToStr(FValue) + ')'. A guarda já existia — mas aninhada dentro do ramofcIn/fcNotIn, valendo só para eles. Por issoInValues(String)eNotIn(String)sempre emitiram a subconsulta verbatim eExists/NotExistsnão: caíam noelseque chamaFParams.Add. O conserto foi hospedar a regra no tipo em vez de no operador — que é também o que faz os dois caminhos (com e sem coleção de parâmetros) dizerem a mesma coisa, já que o caminho inline tratavadftTextassim desde sempre. Os únicos produtores dedftTextsãoIsIn,IsNotIn,IsExistseIsNotExists, então subir a guarda não alcança operador algum além desses quatro.Anti-colateral:
Equal,Like,InValues(array)e todo slot de valor continuam parametrizando byte a byte como antes — travado porTestSlotDeValorContinuaParametrizandoemTest Delphi\Common_tests\test.exists.subquery.matrix.pas, que cai se a guarda alcançardftString.⚠️
Exists(String),NotExists(String),InValues(String)eNotIn(String)são PORTAS DE ESCAPE e portanto PORTAS DE INJEÇÃO por construção. O argumento éStringe vai direto para o SQL, sem bind e sem escape. Isso é deliberado, e é o mesmo contrato deCast(x, 'VARCHAR2')e deIfThen('SALARIO * 1.1'): você escolheu a palavra, a portabilidade e a segurança são suas. Não alimente essas sobrecargas com entrada de usuário não validada — os valores que a subconsulta compara devem vir de binds do consumidor, não de concatenação. Quem quer o slot de valor usaInValues(TArray<…>), que parametriza cada elemento. O contrato está escrito na doc de cada método emFluentSQL.Interfaces.pas.Nenhuma interface ganhou membro nesta mudança — não há
E2291para quem implementaIFluentSQLdo zero. A única alteração de assinatura foi o nome do parâmetro (AValue→ASubQuery), que em Delphi/FPC não participa da compatibilidade. Uma eventual sobrecargaExists(IFluentSQL)— porta portável construída pelo builder, ainda não implementada — seria um BREAKING de API separado.BREAKING CHANGE (SQL emitido) — Oracle: literal de data e de data-hora passou a sair como literal ANSI tipado.
TUtils.DateToSQLFormateTUtils.DateTimeToSQLFormatemitiam paradbnOracleo literal cru entre aspas, e o Oracle recusa esse texto. Medido em Oracle AI Database 26ai Free Release 23.26.2.0.0, com oNLS_DATE_FORMATde fábrica (DD-MON-RR) — o erro é noCREATE TABLE, não noINSERT. Quem compara o SQL gerado com string fixa paradbnOracleprecisa atualizar as expectativas; quem executa o DDL passa a executar SQL que o motor aceita.Construção ( dbnOracle)Antes Depois Motor real .ColumnDate('D').DefaultValue('2024-04-14')"D" DATE DEFAULT '2024-04-14'"D" DATE DEFAULT DATE '2024-04-14'antes: ORA-01861: literal does not match format string.ColumnDateTime('D').DefaultValue('2024-04-14 12:34:56')"D" TIMESTAMP DEFAULT '2024-04-14 12:34:56'"D" TIMESTAMP DEFAULT TIMESTAMP '2024-04-14 12:34:56'antes: ORA-01843: An invalid month was specifiedos outros oito dialetos — inalterado ver matriz abaixo O texto antigo não estava "quase certo": ele dependia de configuração de sessão que o framework não controla. A mesma sentença que morre com
ORA-01861passa a criar a tabela depois deALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD'— medição transcrita na seção I do arquivo-oráculo. O literal ANSI tipado não depende de NLS nenhum.Emitir a forma ANSI para todos os dialetos, que seria a correção óbvia, QUEBRARIA três dos outros seis — e é essa medição que obrigou a correção a ser um ramo de dialeto. Não existe forma aceita pelos sete. A matriz completa, com transcrição literal,
docker run, digest de imagem, versão perguntada a cada motor e controle positivo/negativo, está emTest Delphi\Common_tests\test.date.literal.matrix.sql:Dialeto '2026-08-10'cruDATE '2026-08-10'Oracle 23.26.2.0.0 recusa — ORA-01861aceita SQL Server 2022 (16.0.4265.3) aceita recusa — Msg 128, The name "DATE" is not permitted in this contextMySQL 8.4.11 aceita recusa — ERRO 1067, Invalid default value(só entre parênteses, e aí vira expressão)SQLite 3.53.4 aceita recusa — parse error, nem entre parêntesesPostgreSQL 16.14 aceita aceita Firebird 5.0.4 aceita aceita DB2 12.1.5.0 aceita aceita InterBase não medido — não existe imagem pública não medido O literal cru é aceito por 6 e recusado por 1; o ANSI tipado é aceito por 4 e recusado por 3. A interseção é vazia. Por isso o ramo ficou restrito a
dbnOracle, o único com defeito medido, e os outros seis continuam emitindo byte a byte o que emitiam — travado por mutação dirigida emtest.date.literal.matrix.pas(estender o ramo a qualquer um dos cinco dialetos com serializador DDL derruba a célula correspondente).Alcance da mudança, declarado — são SEIS posições emitidas, não cinco. A correção mora nas duas funções de
FluentSQL.Utils.pas, então alcança tudo que passa por elas:# posição sítio 1 DEFAULTde coluna emCREATE TABLEFluentSQL.DDL.SerializeAbstract.pas:1252 DEFAULTde coluna emALTER TABLE … ADDFluentSQL.DDL.Serialize.Oracle.pas:134→:138→GetColumnDefinition→ o mesmo:1253–6 WHERE(colunaDATEe colunaTIMESTAMP),INSERT … VALUES,UPDATE … SET,BETWEEN— pelo caminho inline deFluentSQL.Operators.pas:232-233seção I.2 do arquivo-oráculo O literal ANSI foi submetido e aceito nas seis. Já o literal cru foi submetido em três:
DEFAULTde coluna,WHEREsobre colunaDATEeWHEREsobre colunaTIMESTAMP— e morre nas três (ORA-01861,ORA-01861,ORA-01843). EmINSERT … VALUES,UPDATE … SETeBETWEENsó a forma ANSI foi submetida; o que essas três linhas do oráculo afirmam é a aceitação do ANSI, e nada sobre a recusa do cru. É provável que o cru também seja recusado ali —ORA-01861é erro de conversão, não de posição —, mas isso fica escrito como provável, não como medido. (Uma versão anterior desta entrada dizia "o cru é recusado em todas": generalização a partir de duas submissões.)A posição 2 é omissão da declaração original desta entrega, apontada e medida pela revisão da T19 — o texto do HEAD altera a tabela e devolve o valor certo, e o texto da base devolve
ORA-01861ali também; a medição é da revisão, não desta entrega. Duas ressalvas de proveniência: o enunciado submetido ali foi reconstruído à mão por leitura deOracle.pas:138+GetColumnDefinition, não copiado da saída do gerador como são os doze da seção IX do oráculo — está medido que a forma é aceita, e é leitura de código que o gerador produza esse texto; e não há célula de teste travando essa posição. (ALTER TABLE … MODIFYrealmente não emiteDEFAULTno serializador do Oracle — nessa parte a declaração original procede.)Sobre o caminho inline, com a redação corrigida:
TFluentSQLOperatorssó tem construtor de dois argumentos, e o único chamador interno sempre passa os params — cair no ramo inline exige passarnilexplicitamente ou usarTFluentSQLOperatordireto. O alcance é estreito, como se dizia; a razão é essa, e não "um construtor sem parâmetros". Nada foi decidido aqui sobre remover o modo inline; a lógica docasedeOperators.pasnão foi tocada.Fronteira declarada:
dbnInterbasenão foi medido (não existe imagem pública) e seu ramo não foi tocado. O ramodbnFirebird/dbnInterbasenão foi mexido por decisão de escopo — esta tarefa nasceu do defeito do Oracle e não foi encarregada de decidir o formato de outro dialeto. Isso não é o mesmo que "não há nada a rever no Firebird": esta entrega mediu apenas que'04/14/2026'→2026-04-14e'12/13/2026'→2026-12-13. A revisão da T19 mediu que'13/12/2026'é RECUSADO comSQLSTATE 22018. O momento da recusa depende do caminho: porCASTela é imediata (o próprioSELECTmorre); peloDEFAULTde coluna — que é o que o framework emite — ela é diferida, oCREATE TABLEpassa e o22018só aparece noINSERT, de modo que o schema nasce com um default que estoura no primeiro uso. A medição é da revisão, não desta entrega, e a falha diferida está catalogada como tarefa própria pelo orquestrador desta fila (sem identificador atribuído até aqui). Uma versão anterior desta entrada afirmava o oposto —'13/12/2026'aceito por fallback DD/MM, "ambiguidade real" — por inferência, a partir de umCREATE TABLEque passou sem que o valor jamais fosse lido; a afirmação foi removida.BREAKING CHANGE (SQL emitido) — SQL Server: coluna computada deixou de carregar o tipo declarado. O T-SQL não aceita tipo em coluna computada: a gramática de
<computed_column_definition>écolumn_name AS computed_column_expression, sem<data_type>— o tipo é derivado da expressão. O FluentSQL emitia paradbnMSSQLum texto que o motor recusa, e o teste que fixava esse texto estava verde. Quem compara o SQL gerado com string fixa paradbnMSSQLprecisa atualizar as expectativas; quem executa o DDL passa a executar SQL que o motor aceita.Construção ( dbnMSSQL)Antes Depois Motor real .ColumnInteger('TOTAL').ComputedBy('QTD * PRECO')[TOTAL] INT AS (QTD * PRECO)[TOTAL] AS (QTD * PRECO)antes: Msg 156, Incorrect syntax near the keyword 'AS'.ColumnInteger('ID')(coluna comum)[ID] INTinalterado aceito nos dois coluna computada nos outros dialetos — inalterado ver matriz abaixo A recusa foi medida com
SET PARSEONLY ON, que só parseia e nem chega a resolver nome — logo é defeito de gramática pura, sem nuance de convenção.Tirar o tipo para todos os dialetos, que seria a correção óbvia, QUEBRARIA o PostgreSQL — e é esse par de medições que obrigou a correção a ser local. Medido em PostgreSQL 16.14:
"TOTAL" GENERATED ALWAYS AS (...) STORED, sem o tipo, devolveERROR 42601: syntax error at or near "ALWAYS". A matriz completa, com transcrição literal,docker rune versão perguntada a cada motor, está emTest Delphi\Common_tests\test.computed.column.matrix.sql:Dialeto forma emitida tipo declarado SQL Server 2022 (16.0.4265.3) [TOTAL] AS (...)proibido — Msg 156se presentePostgreSQL 16.14 "TOTAL" INTEGER GENERATED ALWAYS AS (...) STOREDexigido — 42601se ausenteMySQL 8.4.11 `TOTAL` INT AS (...) VIRTUALaceito Oracle 23.26.2.0.0 "TOTAL" NUMBER(10) GENERATED ALWAYS AS (...) VIRTUALaceito Firebird 5.0.4 "TOTAL" INTEGER COMPUTED BY (...)aceito SQLite 3.53.4 não emite — levanta ENotSupportedExceptionnão se aplica InterBase não medido — não existe imagem pública não medido Dos seis serializadores DDL: 4 carregam o tipo, 1 o recusa (MSSQL), 1 não emite coluna computada (SQLite). Por isso a regra ficou num gancho de dialeto —
TFluentDDLSerializeAbstract.ComputedColumnCarriesType,virtual, defaultTrue, sobrescrito paraFalsesó emFluentSQL.DDL.Serialize.MSSQL.pas. Quem não sobrescreve continua emitindo byte a byte o que emitia: o anti-colateral é estrutural, não apenas testado. O ramo novo emGetColumnDefinitioné guardado por(LComputed <> ''), então coluna comum não passa por ele em dialeto nenhum.Ressalva sobre a linha do PostgreSQL, e ela importa: o enunciado que a suíte fixa para
dbnPostgreSQL—"TOTAL" INTEGER GENERATED ALWAYS AS (QTD * PRECO) STORED— é recusado pelo motor (ERROR 42703: column "qtd" does not exist), porque o gerador delimita o nome da coluna e repassa a expressão crua, que o PG dobra para minúscula. O que a matriz acima afirma sobre o PostgreSQL é apenas que ele exige o tipo, e isso é verdade. O defeito de delimitação é anterior a esta mudança, permanece aberto e é território da tarefa de delimitação de identificador, parada aguardando decisão. Está marcado no ponto exato emtest.computed.column.matrix.pas.Fronteira declarada: a interação entre coluna computada e
MapConstraints(por exemploNOT NULLouDEFAULTjunto deComputedBy) não foi medida em motor nenhum — nenhum teste da suíte exercita a combinação.BREAKING CHANGE (API) —
Delete.From(A).From(B)deixou de emitir SQL e passou a levantarEFluentSQLConstructNotSupported. O que fechou foi a lista de relações doFROMde umDELETE: antes, a segunda chamada deFromnuma seçãoDELETEacumulava a relação e o framework emitia uma lista separada por vírgula.⚠️ Esta guarda cobre a lista de relações do
FROM, e só ela. A porta irmã —Delete.From('A').InnerJoin('B').OnCond(…), que passa porTFluentSQL._CreateJoine não porASTTableNames— ficou aberta de propósito quando esta entrada foi escrita, e foi fechada depois, por guarda própria: ver "JOINdentro de umDELETEdeixou de emitir SQL", no topo desta mesma seção. A previsão registrada aqui de que a resposta lá seria "traduzir" não se confirmou: a medição em motor mostrou que a forma nativa doLeftJoinapaga a tabela inteira em silêncio em dois motores, e a porta foi recusada, não traduzida. O texto original desta ressalva ficou obsoleto e está corrigido.Os sete motores relacionais recusam a lista separada por vírgula por parse — não é "a interseção é vazia", é a união que é vazia: não há um motor sequer em que ele executasse. Medição em motor real, transcrição literal com
docker rune versão perguntada a cada motor emTest Delphi\Common_tests\test.delete.multirelacao.matrix.sql:Dialeto DELETE FROM A AS X, B AS YDELETE FROM A, BDELETE FROM A X, B YSQL Server 2022 (16.0.4265.3) Msg 156Msg 102Msg 102Oracle 23.26.2.0.0 ORA-03048ORA-03048ORA-03048PostgreSQL 16.14 syntax error at or near ","syntax errorsyntax errorMySQL 8.4.11 ERROR 1064ERROR 1064ERROR 1064Firebird 5.0.4 SQL error code = -104-104-104SQLite 3.53.4 near ",": syntax errorsyntax errorsyntax errorDB2 12.1.5.0 SQL0104NSQL0104NSQL0104NInterBase não medido — não existe imagem pública não medido não medido O defeito nunca foi do apelido: sem apelido nenhum,
DELETE FROM A, Bjá derruba o parse nos sete. A guarda, por isso, não olhaAlias.Por que RECUSAR e não traduzir para a forma nativa de cada dialeto. Porque as formas nativas não significam a mesma coisa. Medido com contagem antes/depois,
AeBcom 2 linhas cada:Dialeto Forma nativa Apagou de T-SQL DELETE X FROM A AS X JOIN B AS Y ON ...só de A (A 2→1, B 2→2) PostgreSQL DELETE FROM A AS X USING B AS Y WHERE ...só de A (A 2→1, B 2→2) Oracle 23ai DELETE FROM A X USING B Y WHERE ...só de A (A 3→1, B 2→2) MySQL DELETE X, Y FROM A AS X JOIN B AS Y ON ...das DUAS (A 2→1, B 2→1) T-SQL DELETE X, Y FROM A AS X JOIN B AS Y ON ...Msg 102— recusaFirebird, SQLite, DB2 não têm forma multi-relação nenhuma — São duas construções diferentes: "apagar de uma relação filtrando pela outra" e "apagar das duas". A segunda existe em 1 dos 7, e o T-SQL a recusa explicitamente. Mapear uma única chamada do builder para elas trocaria "SQL que não executa em lugar nenhum" por "SQL que executa apagando coisas diferentes conforme o banco" — o primeiro defeito grita na primeira execução, o segundo é silencioso e destrutivo.
E o que fecha o assunto: a chamada não diz qual das duas foi pedida.
Delete.From('A','X').From('B','Y')não tem designador de alvo, não tem condição de junção própria da seção e não tem marcador de relação auxiliar. Não distingue a semântica 1 da 2, nem "A é alvo e B é filtro" de "B é alvo e A é filtro". Construção cujo significado ninguém sabe declarar não pode ter tradução correta.Quem é atingido: ninguém em produção — o SQL antigo não executava em motor nenhum. Atinge quem tem teste comparando esse texto com string fixa, e quem constrói a query dinamicamente e pode chamar
Fromduas vezes por caminho de código. O que fazer: emitir umDELETEpor relação, ou restringir a única relação alvo peloWHERE— inclusive com subconsulta, que é portável nos sete.Onde a guarda mora:
TFluentSQL.From(const ATableName: String): IFluentSQL(Source\Core\FluentSQL.pas), não num serializador. Falha na chamada que errou, vale para todos os dialetos sem uma linha por driver, e — isto importa para quem procura o segundo BREAKING — nenhuma interface publicada ganhou membro nesta mudança:IFluentSQLSerialize,IFluentSQLDeleteeIFluentSQLestão idênticas, e portanto não há quebra de compilaçãoE2291aqui. O único tipo novo é a classe de exceçãoEFluentSQLConstructNotSupported, que é aditiva (ver Added).O que NÃO mudou, e é medido por teste em todo dialeto registrado:
Delete.From('A'),Delete.From('A','AP')eSelect.All.From('A').From('B')(SELECT * FROM A, B, forma válida e antiga) saem byte a byte iguais.MongoDB: também levanta, pela guarda ser de núcleo. Antes ele descartava a segunda coleção em silêncio (o MQL emitido citava apenas a primeira). Fora da interseção relacional e fora de qualquer contagem.
Isto fecha a fronteira declarada e não medida pela entrada do apelido em
DELETE, logo abaixo — a linhaDelete.From('A','X').From('B','Y')da tabela dela dizia "inalterado / não medido", e é essa fronteira que esta entrada mede e encerra.BREAKING CHANGE (SQL emitido) — SQL Server:
Delete.From(tabela, apelido)mudou de FORMA. O T-SQL não aceita apelido preso aoDELETE FROM; o alvo doDELETEtem de ser o apelido, e é oFROMque carrega a tabela apelidada. O FluentSQL emitia paradbnMSSQLum texto que o motor recusa — defeito pré-existente, não introduzido pela correção do apelido no Oracle. Quem compara o SQL gerado com string fixa paradbnMSSQLprecisa atualizar as expectativas; quem executa a consulta passa a executar SQL que o motor aceita.Construção Antes (MSSQL) Depois (MSSQL) Motor real Delete.From('A','AP').Where(...)DELETE FROM A AS AP WHERE ...DELETE AP FROM A AS AP WHERE ...antes: Msg 156, Incorrect syntax near the keyword 'AS'Delete.From('A').Where(...)DELETE FROM A WHERE ...inalterado aceito nos dois Delete.From('A','X').From('B','Y')DELETE FROM A AS X, B AS Y ...inalterado não medido — ver fronteira abaixo From('A','AP')emSELECTSELECT * FROM A AS APinalterado aceito nos dois Column('NOME').Alias('N')SELECT NOME AS Ninalterado aceito nos dois Tirar o
AS, que foi o que resolveu o Oracle, NÃO resolve o T-SQL — e é esse par de medições que separa esta correção da anterior:DELETE FROM A AS APdevolveMsg 156eDELETE FROM A APdevolveMsg 102. As duas formas que o núcleo sabia produzir são recusadas; sóDELETE AP FROM A AS APexecuta.Os outros seis dialetos relacionais não mudaram uma vírgula, e isso é resultado de medição, não de analogia. A matriz completa, com transcrição literal,
docker rune versão perguntada a cada motor, está emTest Delphi\Common_tests\test.delete.alias.matrix.sql:Dialeto DELETE FROM t AS apDELETE FROM t apDELETE ap FROM t AS apSQL Server 2022 (16.0.4265.3) Msg 156Msg 102executa Oracle 23.26.2.0.0 ORA-03048executa ORA-03048PostgreSQL 16.14 executa executa syntax error at or near "AP"MySQL 8.4.11 executa executa executa Firebird 5.0.4 executa executa SQL error code = -104SQLite 3.53.4 executa syntax errorsyntax errorDB2 12.1.5.0 executa executa SQL0104NInterBase não medido — não existe imagem pública não medido não medido A interseção é vazia: não há forma que os sete aceitem. A do T-SQL é aceita por 2 de 7; a que o FluentSQL já emitia, por 5 de 7; a do Oracle, por 4 de 7. SQL Server e SQLite se excluem em qualquer combinação — um recusa a ausência do
AS, o outro recusa oAS. Por isso a forma ficou no serializador do dialeto (IFluentSQLSerialize.DeleteClause, virtual na base, sobrescrita só emFluentSQL.SerializeMSSQL.pas) e não virou regra única no núcleo. Recusar apelido emDELETEcom exceção nomeada também foi considerado e descartado: 5 dos 7 aceitam a forma atual, e retirar a funcionalidade deles para uniformizar seria perda maior que a correção.A regra não foi para o qualificador, pela mesma razão da correção do Oracle:
TFluentSQLSelectDB2instancia o qualificador do Oracle (FluentSQL.SelectDB2.pas:46) — hospedar a regra ali faria o DB2 herdar calado a forma do T-SQL. Medido: o DB2 recusa essa forma (SQL0104N) e continua emDELETE FROM t AS ap.Fronteira declarada: com mais de uma relação no
DELETE(Fromchamado duas vezes), odbnMSSQLcai de propósito na forma da base e emiteDELETE FROM A AS X, B AS Y— igual a antes. Qual das relações seria o alvo doDELETEé decisão de convenção, não conserto silencioso, e essa forma não foi executada em motor nenhum nesta correção.BREAKING CHANGE (API) —
IFluentSQLSerializeganhou o métodofunction DeleteClause(const ADelete: IFluentSQLDelete): String;. Acrescentar método a interface publicada quebra quem a implementa do zero. É quebra de compilação, distinta e independente do BREAKING de SQL emitido da entrada acima: aquela atinge quem compara texto gerado paradbnMSSQL, esta atinge quem escreve serializador de dialeto próprio — inclusive quem não usaDELETEcom apelido em lugar nenhum.Quem é atingido: quem DECLARA
IFluentSQLSerializenuma classe própria. Só isso. Não é preciso registrar a classe, instanciá-la, nem usá-la — uma implementação engavetada, ou registrada por outro caminho que nãoTFluentSQLRegister.RegisterSerialize(FluentSQL.Register.pas:278), quebra igual. Quem descende deTFluentSQLSerialize(FluentSQL.Serialize.pas:28) não é atingido: herda o novo método de graça. É o caso dos nove serializadores de driver do repositório, e de qualquer subclasse externa.Medido nos dois cenários, não deduzido. Uma classe declarando exatamente os quatro membros anteriores (
AsString,Merge,QuotedName,RelationAliasKeyword) compila limpo contra9d0407ce, contra esta branch, para em:terceiro.dpr(9) Error: E2291 Missing implementation of interface method IFluentSQLSerialize.DeleteClause <- com RegisterSerialize soDeclara.dpr(8) Error: E2291 Missing implementation of interface method IFluentSQLSerialize.DeleteClause <- SEM registrar, sem instanciar, e sem FluentSQL.Register no usesO segundo caso é o que define a população atingida: a quebra é da declaração, não do uso. Uma redação anterior desta entrada dizia "implementa do zero e injeta por
RegisterSerialize" — estreitava a população e convidava quem não injeta a concluir "não é comigo".O que fazer: passar a descender de
TFluentSQLSerialize— que já traz a forma padrão — ou declarar o método. O corpo honesto para quem não tem regra própria éResult := ADelete.Serialize;, que é literalmente o que a base faz e reproduz o comportamento anterior byte a byte.Não há ressalva de runtime aqui, ao contrário do
Cast— e a diferença não é "concreto × abstrato". Nenhum dos dois é declaradoabstract:DeleteClauseévirtual(FluentSQL.Serialize.pas:36) e as duas sobrecargas deCasttambém são (FluentSQL.FunctionsAbstract.pas:73e:74). A distinção que importa é o que o corpo da base faz:Membro Corpo da base Quem herda e não sobrescreve DeleteClauseResult := ADelete.Serialize;(FluentSQL.Serialize.pas:196-199)funciona — emite o texto de sempre Cast(String, String)única instrução é raise EAbstractError(FluentSQL.FunctionsAbstract.pas:129)compila limpo e explode na primeira chamada Cast(String, TFluentSQLDataFieldType, Integer)idem ( FluentSQL.FunctionsAbstract.pas:134)idem Vale a pena saber distinguir as duas: é a diferença entre "a base faz o trabalho" e "a base é um contrato que levanta", e ela reaparece em toda a matriz driver × função — é exatamente o que separa as funções cujo núcleo emite ANSI direto das que delegam ao driver.
Está aqui e não em Added pela régua que esta mesma lista já aplicou três vezes —
IFluentSQLSelectQualifierscomRequestsZeroRows,IFluentSQLFunctionscomCast, eIFluentSQLCriteriaCasecom os dois membros do slot de valor: "acrescentar método a interface publicada não pode ser rodapé". A régua não muda de interface para interface.BREAKING CHANGE (SQL emitido) — Oracle: o apelido de TABELA perdeu a palavra
AS. O Oracle não aceitaASantes de apelido de tabela, view ou subconsulta, e o núcleo emitiaASpara todo apelido sem consultar dialeto nenhum — a linha eraResult := TUtils.Concat([Result, 'AS', FAlias]), emFluentSQL.Name.pas:114na árvore anterior à correção (commiteb48337); no HEAD atual essa linha não existe mais, e o ponto equivalente éFluentSQL.Name.pas:161, já lendoFAliasKeyword. TodoFrom(tabela, apelido)e as quatro sobrecargas de join com apelido (InnerJoin/LeftJoin/RightJoin/FullJoin) produziam SQL que o Oracle recusa. Quem compara o SQL gerado com string fixa paradbnOracleprecisa atualizar as expectativas; quem executa a consulta passa a executar SQL que o motor aceita.Construção Antes (Oracle) Depois (Oracle) Motor real From('CLIENTES','CLI')SELECT * FROM CLIENTES AS CLISELECT * FROM CLIENTES CLIantes: ORA-03048LeftJoin('B','X')... LEFT JOIN B AS X ON ...... LEFT JOIN B X ON ...antes: ORA-02000InnerJoin/RightJoin/FullJoin('B','X')... JOIN B AS X ON ...... JOIN B X ON ...antes: ORA-02000Delete.From('A','AP')DELETE FROM A AS AP WHERE ...DELETE FROM A AP WHERE ...antes: ORA-03048From('(SELECT ...)')+Alias... AS S... Santes: ORA-03048Column('NOME').Alias('N')SELECT NOME AS Ninalterado aceito nos dois O apelido de COLUNA não mudou, em dialeto nenhum — e é esse contraste que sustenta a correção. A documentação do
SELECTdo Oracle definet_alias(tabela/view/subconsulta) sem citarASem momento algum, e definec_alias(coluna) dizendo "TheASkeyword is optional": declara opcional onde é permitido e omite onde não é. ComoTFluentSQLNameserve os dois papéis com o mesmoSerialize, tirar oASdo serializador consertaria a tabela e quebraria a coluna.Os códigos acima foram medidos, não presumidos — o palpite corrente era
ORA-00933e não é nenhum dos dois. As execuções brutas, incluindo odocker rune a versão do motor (Oracle AI Database 26ai Free Release 23.26.2.0.0), estão emTest Delphi\Common_tests\test.alias.oracle.sql.Os outros seis dialetos relacionais não mudaram uma vírgula. A alternativa "emitir sem
ASem todos" — gramaticalmente válida nos sete — foi recusada: trocaria o texto de seis dialetos para consertar um. A palavra passou a vir deIFluentSQLSerialize.RelationAliasKeyword('AS'na base,''só no Oracle). Ela mora no serializador, e não no qualificador, por duas razões: o qualificador não alcança oJOIN(serializado porFluentSQL.Joins.pas), eTFluentSQLSelectDB2instancia o qualificador do Oracle (FluentSQL.SelectDB2.pas:46) — hospedar a regra ali faria o DB2 herdar calado a forma do Oracle. Medido: o DB2 continua emtabela AS apelido.BREAKING CHANGE (SQL emitido) — a paginação mudou de forma em 5 dos 7 dialetos ativos. O framework anunciava paginação nos sete e ela só estava correta em dois (PostgreSQL e MongoDB). Quem compara o SQL gerado com string fixa precisa atualizar as expectativas; quem executa a consulta passa a executar SQL que o motor aceita. Com
Skip(20)sozinho, antes → depois:Dialeto Antes Depois MSSQL ...) AS T WHERE (ROWNUMBER > 20)(subconsulta +ROW_NUMBER())... ORDER BY (SELECT NULL) OFFSET 20 ROWSOracle ...) AND ROWINI > 20—ANDsemWHERE,ORA-03048... OFFSET 20 ROWSSQLite SELECT OFFSET 20 * FROM T— posição errada na gramática, inválido sempre... LIMIT -1 OFFSET 20MySQL ... OFFSET 20—OFFSETsemLIMIT,ERROR 1064... LIMIT 18446744073709551615 OFFSET 20Firebird SELECT SKIP 20 * FROM Tinalterado PostgreSQL ... OFFSET 20inalterado MongoDB "skip":20inalterado Os tetos do MySQL (2^64−1) e do SQLite (
LIMIT -1) não são invenção: são as receitas documentadas em cada manual para "todas as linhas a partir de um deslocamento", e são necessários porque nesses dois motoresOFFSETnão é cláusula independente — só no PostgreSQL é. O Firebird não migrou paraOFFSET/FETCHde propósito:FIRST/SKIPfunciona em toda versão (inclusive 2.5) e aceita expressão, enquanto oOFFSET/FETCHdo Firebird só aceita literal ou parâmetro.No MSSQL, como
<offset_fetch>só existe dentro de umORDER BY, semOrderBydo usuário é emitidoORDER BY (SELECT NULL)— preenchimento gramatical, que não promete determinismo e não acrescenta operadorSortao plano. SobDISTINCTouUNIONo motor recusa(SELECT NULL)(Msg 145eMsg 104) e é emitidoORDER BY 1, o único item que está sempre na lista de seleção. Todas as formas foram medidas em motor real; osdocker run, versões e saídas brutas estão emTest Delphi\Common_tests\test.pagination.<dialeto>.sql.BREAKING CHANGE (SQL emitido) —
First(0)passou a devolver zero linhas nos 7 dialetos.First(0)é pedido legítimo e distinto de "semFirst", como o próprioTFluentSQLPaginationdeclara. Estava errado em dois dialetos, cada um de um jeito:Dialeto Antes Depois MSSQL ... OFFSET 0 ROWS FETCH NEXT 0 ROWS ONLY—Msg 10744, recusadoSELECT TOP 0 ...→ 0 linhasMSSQL, com UNION/UNION ALL/EXCEPT/INTERSECTidem ... ORDER BY 1 OFFSET 9223372036854775807 ROWS→ 0 linhasMongoDB ( find)"limit":0— devolvia a coleção INTEIRA, em silêncio"skip":9223372036854775807→ 0 documentosMongoDB ( aggregate){"$limit":0}—the limit must be positive{"$skip":9223372036854775807}→ 0 documentosFirebird / MySQL / SQLite / PostgreSQL / Oracle já corretos inalterados No MongoDB
limit: 0significa sem limite — o usuário pedia nada e recebia tudo, sem erro. Era o único dos sete em queFirst(0)falhava calado, e os dois caminhos do próprio driver (findeaggregate) discordavam entre si sobre o queFirst(0)queria dizer.No SQL Server a restrição é do literal, não da semântica: o mesmo
FETCHcom o contador vindo de uma variávelBIGINTvalendo0é aceito e devolve zero linhas. A forma usada éSELECT TOP 0, e ela descarta oSkip(n)do usuário — pular n linhas de um conjunto vazio dá o mesmo conjunto vazio, e é esse descarte que evita oMsg 10741(TOPnão coexiste comOFFSET). De quebra, dispensa a cláusulaORDER BYde preenchimento, que só existia para hospedar oOFFSET/FETCH.A única combinação em que
TOP 0não serve é a operação de conjunto: oTOPpertence a uma query specification e limita só o ramo em que está escrito — medido,SELECT TOP 0 * FROM T UNION SELECT * FROM Udevolve as 60 linhas deU. Só aí entra a caudaOFFSET 9223372036854775807 ROWS, que custa uma varredura completa da tabela, contra I/O zero doTOP 0— comORDER BY, a forma cara ainda acrescenta uma Worktable, ou seja, ordenação. A afirmação é o contraste:TOP 0não lê a tabela, a cauda lê tudo. O número absoluto de leituras lógicas depende da largura da linha e do tamanho da massa — na medição registrada emtest.pagination.mssql.sql(200 mil linhas, uma colunaINT) deu 767, e uma revisão independente mediu 446 numa tabela mais estreita. Não trate o número como propriedade da forma; trate o contraste. O custo ficou confinado a esse caso em vez de valer para todoFirst(0)—First(pageSize)compageSizezerado dentro de um laço, numa tabela grande, é incidente de produção.Também medido e recusado:
FETCH NEXT (SELECT 0) ROWS ONLY, aceito e barato, mas só quando oOFFSETé o literal0. Todas as medições estão emtest.pagination.mssql.sql, parte Z — inclusive os caminhos não seguidos, para que quem pensar emTOP 0ou em(SELECT 0)encontre a medição pronta em vez de refazê-la.No MongoDB o
Skip(n)também é descartado, pelo mesmo motivo. Os outros cinco dialetos preservam os dois números, porque conseguem exprimir o zero pelo limite.Skip(0)foi medido correto nos 7 antes e depois e não mudou — é "não pule nada", não "semSkip".BREAKING CHANGE (SQL emitido) — Firebird:
FIRST/SKIPpassaram a preceder oDISTINCT. EraSELECT DISTINCT FIRST 3 SKIP 20 ..., forma que o Firebird 5.0.4 recusa com-104 Token unknown; a gramática éSELECT [FIRST m] [SKIP n] [{DISTINCT | ALL}] <colunas>. Toda consultaSelect.Distinctcom paginação neste driver era rejeitada pelo motor.MSSQL, Oracle e DB2:
DISTINCTpassou a preceder a lista de colunas. EmitiamSELECT NOME DISTINCT FROM T. Não dependia de paginação:Select.Distinctsozinho já saía assim.BREAKING CHANGE (comportamento) —
TFluentSQLRegister.Functionsdeixou de devolvernil. Passa a levantarEFluentSQLDriverNotRegistered(classe nova emFluentSQL.Interfaces.pas). Antes, onilera desreferenciado emFluentSQL.Functions.pase chegava ao consumidor comoEAccessViolationopaca.SelecteSerializepassaram deExceptioncrua para a mesma classe nomeada. Quem captura esses erros para os traduzir em erro de domínio deve rever otry..except.CeileLengthdeixaram de ser emitidos como SQL ANSI fixo pelo núcleo e passaram a delegar ao driver. Corrige SQL inválido gerado em silêncio:CEIL(...)não existe em T-SQL (agoraCEILING), eLENGTH(...)não existe nem em T-SQL (agoraLEN) nem no núcleo do Firebird (agoraCHAR_LENGTH).BREAKING CHANGE (API) —
IFluentSQLSelectQualifiersganhou o métodoRequestsZeroRows. Acrescentar método a interface publicada quebra quem a implementa do zero.Impacto real: baixo, e provavelmente não é você. O único implementador é
TFluentSQLSelectQualifiers, no core, e os 9 qualificadores de driver descendem dela sobrescrevendo apenasSerializePagination— herdam o novo método de graça, e o mesmo vale para qualquer subclasse externa. Só quebra quem implementaIFluentSQLSelectQualifiersinteira do zero e a injeta com umIFluentSQLSelectpróprio viaRegisterSelect. O que fazer nesse caso: implementarfunction RequestsZeroRows: BooleancomoHasFirst and (First = 0)sobre a sua coleção — ou passar a descender deTFluentSQLSelectQualifiers.Está aqui e não em Added porque é a régua que esta própria entrega estabelece: se ela rotula como BREAKING até a troca de classe de exceção que pede revisão de
try..except, acrescentar método a interface publicada não pode ser rodapé.O método responde "o usuário pediu
First(0)?". É pergunta sobre a coleção de qualificadores, não sobre dialeto: a forma de exprimir zero linhas varia (TOP 0,LIMIT 0,FIRST 0, pular tudo), o pedido é o mesmo nos sete. E não éFirst = 0, éHasFirst and (First = 0)— "não pediuFirst" e "pediuFirst(0)" são coisas diferentes, e só a primeira devolve o conjunto todo.BREAKING CHANGE (SQL emitido) —
IFluentSQLMerge.Update/.Insertpassaram a parametrizar valores string. Valores numéricos já viravam:pN; strings iam verbatim para o texto do SQL, sem aspas e sem escape. Quem compara o SQL gerado com string fixa precisa atualizar as expectativas:Chamada Antes Depois .Update(['NOME', 'TESTE', 'VALOR', 10.5])SET [NOME] = TESTE, [VALOR] = :p1SET [NOME] = :p1, [VALOR] = :p2.Insert(['ID', 1, 'NOME', 'TESTE'])VALUES (:p1, TESTE)VALUES (:p1, :p2)Isto não era só um buraco de segurança latente — era funcionalidade quebrada. O caso benigno já não funcionava em motor nenhum:
SET [NOME] = TESTEéMsg 207, Invalid column name 'TESTE'no SQL Server, eO'BriendavaMsg 105, Unclosed quotation mark. Com valor hostil, era injeção executável: medido em SQL Server 2022,.Update(['NOME', '1; DROP TABLE USERS; --'])derrubou a tabela. Depois da correção o mesmo payload chega ao banco como dado da coluna. Oráculo completo, comdocker run, versão do motor e saída bruta antes e depois:Test Delphi\Common_tests\test.merge.mssql.sql.A regra é a que o overload tipado
SetValue(const AColumnName, AColumnValue: String)já seguia: no array de.Update/.Insertos slots ímpares são nomes de coluna (identificadores, seguem literais) e os pares são valores (sempre:pN).A fronteira, agora que o overload
array of constdeSetValue/Valuestambém mudou (entrada abaixo), é uma regra, não uma lista curta:- Parametrizam string os quatro pontos em que o
array of consté comprovadamente uma lista de valores:Merge.Update,Merge.Insert,SetValue(nome, [...])eValues(nome, [...]). Esses quatro, e só esses, passam porTUtils.SqlArrayOfConstToParameterizedValue. - Continuam literais — string interpolada verbatim no texto do SQL — todos os demais
array of const, sem exceção, porque todos estão em posição de expressão, onde a string pode legitimamente ser fragmento de SQL (identificador, operador, trecho). O critério mecânico é: passa porTUtils.SqlArrayOfConstToParameterizedSql. Hoje são 17, e a versão anterior desta entrada listava só 6 — omitia 11, entre elesAndOpe, que é a forma mais comum da API logo depois doWhere. A lista completa, conferida no código e executada uma a uma:
# Ponto de entrada Implementado em 1 IFluentSQL.Where(array)FluentSQL.pas:14722 IFluentSQL.AndOpe(array)FluentSQL.pas:3683 IFluentSQL.OrOpe(array)FluentSQL.pas:3734 IFluentSQL.Column(array)FluentSQL.pas:6865 IFluentSQL.Having(array)FluentSQL.pas:10186 IFluentSQL.OnCond(array)FluentSQL.pas:4217 IFluentSQL.CaseExpr(array)FluentSQL.pas:3438 IFluentSQL.ForDialectOnly(dialeto, array)FluentSQL.pas:3269 IFluentSQL.Expression(array)FluentSQL.pas:92210 IFluentSQLCriteriaExpression.AndOpe(array)FluentSQL.Expression.pas:26111 IFluentSQLCriteriaExpression.OrOpe(array)FluentSQL.Expression.pas:34412 IFluentSQLCriteriaExpression.Ope(array)FluentSQL.Expression.pas:35813 IFluentSQLCriteriaExpression.Fun(array)FluentSQL.Expression.pas:31814 IFluentSQLCriteriaCase.When(array)FluentSQL.Cases.pas:66215 IFluentSQLCriteriaCase.AndOpe(array)FluentSQL.Cases.pas:28516 IFluentSQLCriteriaCase.OrOpe(array)FluentSQL.Cases.pas:59917 IFluentSQLMerge.On(array)FluentSQL.Merge.pas:376Quatro amostras do que os 11 omitidos emitem de fato, medidas com o payload
x'; DROP TABLE U; --:AndOpe(array) => SELECT * FROM T WHERE (A = :p1) AND (NOME = x'; DROP TABLE U; --) params=1 OrOpe(array) => SELECT * FROM T WHERE ((A = :p1) OR (NOME = x'; DROP TABLE U; --)) params=1 Expression(array) => SELECT * FROM T WHERE NOME = x'; DROP TABLE U; -- params=0 ForDialectOnly(array) => SELECT * FROM TOPTION(x'; DROP TABLE U; --) params=0Se você audita a fronteira, audite a regra, não a lista: qualquer
array of constque não seja um dos quatro slots de valor acima interpola string verbatim. Não passe entrada não confiável por nenhum deles. O caminho seguro para expressão está sob tarefa própria.- Parametrizam string os quatro pontos em que o
BREAKING CHANGE (SQL emitido) —
SetValue(nome, array of const)eValues(nome, array of const)passaram a parametrizar valores string. É o mesmo defeito da entrada acima, noINSERT/UPDATEcomum em vez doMERGE, e a régua é a mesma: string deixou de ir verbatim e passou a:pN. Quem compara o SQL gerado com string fixa precisa atualizar as expectativas:Chamada Antes Depois .SetValue('NOME', ['TESTE']).SetValue('NIVEL', [7])VALUES (TESTE, :p1)— 1 parâmetroVALUES (:p1, :p2)— 2 parâmetros.Values('NOME', ['ANA'])VALUES (ANA)— 0 parâmetrosVALUES (:p1)— 1 parâmetroA assimetria era dentro do mesmo slot:
.Values('NIVEL', [7])já saía como:p1, enquanto.SetValue('NOME', ['TESTE'])saía como o textoTESTEcru. O numérico parametrizava, a string não. E é posição de valor por construção — o próprio_InternalSeta afirma com_AssertSection([secInsert, secUpdate]), ou seja, o array é o lado direito deCOLUNA = ...e não tem como ser fragmento de SQL.Também aqui era funcionalidade quebrada, e não só risco latente. Medido em SQL Server 2022: o caso benigno
.SetValue('NOME', ['TESTE'])emitiaINSERT INTO USERS (NOME) VALUES (TESTE)→Msg 207, Invalid column name 'TESTE'; eO'BriendavaMsg 105, Unclosed quotation mark. Com valor hostil era injeção executável — mas o payload que funciona aqui não é o mesmo doMERGE: numa listaVALUES (...)a aspa simples abre um literal que engole o resto do batch, entãox'; DROP TABLE USERS; --apenas quebra o comando. O payload que executa fecha o parêntese:.SetValue('NOME', ['1); DROP TABLE USERS; --'])emitiaINSERT INTO USERS (NOME) VALUES (1); DROP TABLE USERS; --),(1 rows affected)sem erro, e a tabela foi dropada. Depois da correção o mesmo payload chega ao banco como dado da coluna. Oráculo comdocker run, versão do motor e saída bruta antes e depois:Test Delphi\Common_tests\test.setvalue.mssql.sql.SetValue/Valuestambém passaram a levantarEArgumentExceptionem formas que antes saíam caladas — é a entrada irmã da doMERGE, logo abaixo, e vale para oINSERT/UPDATEcomum, que é o caminho mais trafegado dos dois:Chamada Antes (emitido) Depois .SetValue('NOME', [nil])/.Values('NOME', [nil])—nilem posição de valorVALUES (:p1), comp1 = '00000000'EArgumentException.SetValue('X', [umObjeto])— instância em posição de valorVALUES (:p1), comp1= oClassNameEArgumentException.SetValue('X', [TMinhaClasse])— referência de classeVALUES (:p1), comp1= oClassNameEArgumentException.SetValue('X', [Unassigned])— variante vaziaVALUES (:p1), comp1 = ''(string vazia, nãoNULL)EArgumentException.SetValue('X', [Null])— varianteNullEVariantTypeCastErrorda RTLEArgumentException.SetValue('X', [])— lista vaziaINSERT INTO T (X) VALUES ()/UPDATE T SET X =EArgumentException.SetValue('D', ['CURRENT','TIMESTAMP'])— mais de um valorVALUES (:p1 :p2)— placeholders justapostos, sem vírgulaEArgumentExceptionO
nilnão é novidade desta rodada, mas nunca tinha sido declarado nem testado neste caminho. O ramovtPointerdeTUtils._StringVarRecAsParamé compartilhado peloMERGEe porSetValue/Values, então a guarda introduzida para oMERGEjá valia aqui desde então — mas a entrada do CHANGELOG falava só em "chamadas deMERGE", e a mutação daquele ramo só derrubava testes deMERGE: a guarda do caminho mais usado estava sem oráculo nenhum. Agora tem, emTest Delphi\Common_tests\test.core.params.pas.As quatro linhas do meio são a mesma corrupção do
nil, por outras portas — e essas portas estavam abertas. Objeto, referência de classe eUnassignednão levantavam nada:TUtils._VarRecToStringconverte os três em texto plausível (ClassName,ClassName, string vazia) e o valor ia para a coluna como dado, com o SQL bem-formado e nenhum motor reclamando. ONullvariante já falhava, mas comEVariantTypeCastErrorda RTL, cuja mensagem não nomeia a chamada que a causou — quem capturaEArgumentExceptiondas demais guardas não o pegava. Os cinco passaram a levantar no mesmo lugar,TUtils._AssertValueSlotCarriesData(FluentSQL.Utils.pas:192-224). Quenil/Null/Unassigneddevessem virarNULLem vez de levantar continua sendo decisão de convenção não tomada — os testes travam o comportamento atual, não o abençoam.vtInterfaceficou de fora de propósito: já levanta hoje, em_VarRecToString, então não há corrupção silenciosa a fechar — só a classe e a mensagem são pobres.As duas últimas são guardas de cardinalidade, e existem porque o
CHANGELOGafirmava que "a régua é a mesma" doMERGEenquanto o irmão seguia emitindo SQL inválido em silêncio. Medido em execução real, seis motores, antes de decidir a forma:- Lista vazia — zero dos seis aceitam. MSSQL 2022
Msg 102 near ')'/near ';'; PostgreSQL 16.14syntax error at or near ")"/near ";"; Oracle Free 23ORA-00936: missing expression(as duas); Firebird 5.0.4-104 Token unknown ')'/-104 Unexpected end of command; MySQL 8.4.11ERROR 1136/ERROR 1064; SQLite 3.53.4Parse error near ")"/near ";". - Mais de um valor — cinco dos seis recusam, e o sexto é o motivo mais forte para a guarda. MSSQL
Msg 102 near '@p2'; PostgreSQLsyntax error at or near "$2"; Firebird-104 Token unknown '?'; MySQLERROR 1064 near '?)'; SQLiteParse error near "?". O Oracle não recusa por gramática: ele lê:p1 :p2como bind + variável indicadora (:host:indicator), ou seja, um valor. Com duas colunas isso viraORA-00947: not enough values; com uma coluna a forma é aceita — medido,INSERT INTO T2 (D) VALUES (:p1 :p2)responde1 row created, grava o valor de:p1e descarta:p2sem erro nenhum. Perda silenciosa de valor, que é pior que o erro de sintaxe dos outros cinco.
Controle acompanhando as duas:
VALUES (:p1, :p2)com vírgula executa em MSSQL, PostgreSQL, MySQL, SQLite e Oracle. No Firebird, dito com precisão, não chega a executar: peloisqlo comando atravessa a gramática e para na ligação dos binds —SQLSTATE 07002,No SQLDA for input values provided—, porque oisqlnão liga parâmetro. É fase diferente do-104 Token unknownque a forma justaposta recebe no mesmo cliente, e é justamente esse contraste que sustenta a leitura: a recusa é da forma justaposta, e não doINSERT. Saída bruta e osdocker execexatos emTest Delphi\Common_tests\test.setvalue.mssql.sql.Que um motor aceite não enfraquece a guarda, é o que a justifica: o único dialeto que não protege o consumidor pela sintaxe é justamente o que precisa da proteção na biblioteca. E a chamada que produzia isso —
.SetValue('D', ['CURRENT','TIMESTAMP'])— parece correta a quem a escreve: tem parâmetros de verdade eParams.Count = 2.O que fazer: passe exatamente um valor por coluna; omita a coluna se ela deve ficar
NULL; e, se o que você queria era uma expressão (CURRENT_TIMESTAMP,A + 1), veja o parágrafo seguinte — não há forma de exprimi-la em slot de valor.O que fazer (parametrização): se você dependia de passar fragmento de SQL por esse array — por exemplo
.SetValue('DATA', ['CURRENT_TIMESTAMP'])— ele agora vira dado, e a coluna recebe a stringCURRENT_TIMESTAMP. Não há substituto hoje: o overload tipadoSetValue(const AColumnName, AColumnValue: String)também parametriza (FluentSQL.pas:405-412), e sempre parametrizou. Ou seja, oINSERT/UPDATEdo FluentSQL não exprime expressão em slot de valor — nem antes nem depois; o que existia era um caminho que funcionava por acidente, e apenas quando o texto passado calhava de ser SQL válido no dialeto alvo. Isso está registrado em Known issues como a distinção valor × expressão, que é tarefa de arquitetura própria.- Lista vazia — zero dos seis aceitam. MSSQL 2022
BREAKING CHANGE (comportamento) — nove formas de
MERGEque não levantavam nada passaram a levantarEArgumentException. Quase todas emitiam SQL que nenhum motor executa, e o faziam em silêncio: o erro só aparecia no banco do consumidor, longe da linha que o causou. (A tabela abaixo é a lista completa; uma versão anterior desta entrada dizia "quatro" e listava cinco linhas, e a seguinte dizia "seis" antes de os irmãos donilserem medidos.)Chamada Antes (emitido) Depois .Update(['NOME'])— contagem ímparSET [NOME] =EArgumentException.Insert(['ID', 1, 'NOME'])— contagem ímparVALUES (:p1, )EArgumentException.Update([])/.Update— lista vazia ou sem argumentosUPDATE SET ;EArgumentException.Insert([])/.Insert— lista vazia ou sem argumentosINSERT;EArgumentException.Update(['NOME', nil])—nilem posição de valorSET [NOME] = :p1, comp1 = '00000000'EArgumentException.Update(['NOME', umObjeto])— instância em posição de valorSET [NOME] = :p1, comp1= oClassNameEArgumentException.Update(['NOME', TMinhaClasse])— referência de classeSET [NOME] = :p1, comp1= oClassNameEArgumentException.Update(['NOME', Unassigned])/.Update(['NOME', Null])— variante sem dado:p1 = ''no primeiro caso;EVariantTypeCastErrorda RTL no segundoEArgumentException.Into(…).Using(…).On(…)sem nenhumWhenMatched/WhenNotMatchedMERGE INTO [T] AS [t] USING [S] AS [s] ON (…);— só o cabeçalhoEArgumentExceptionEstá aqui, e não em Fixed, porque é a régua que esta própria entrega estabelece: se ela rotula como BREAKING até a troca de classe de exceção que pede revisão de
try..except(EFluentSQLDriverNotRegistered,EFluentSQLStatementNotSupported), então sair de nenhuma exceção para exceção lançada na chamada — que derruba código que hoje atravessa esse caminho semtrynenhum — não pode ser rodapé de Fixed..Update([])e.Updateproduzem o mesmo texto — as duas chegam ao serializador com zero pares — e por isso caem na mesma regra. A afirmação anterior de que "contagem par, inclusive a lista vazia, continua passando" era falsa: zero é par, mas serializa comoUPDATE SET ;, exatamente o SQL inválido que a guarda existe para impedir. Contagem par e não vazia é que continua passando.Medido em execução real, as duas formas nuas, seis motores, zero aceitam: SQL Server 2022
Msg 102; PostgreSQL 16.14syntax error at or near ";"; Oracle Free 23.26ORA-00921eORA-00926 Missing VALUES or SET keyword; Firebird 5.0.4-104 Unexpected end of command; MySQL 8.4.11 e SQLite 3.53.4 recusam a palavraMERGEinteira, que não existe nesses dois. Dois controles acompanham a medição para que a leitura não ultrapasse o medido: no PostgreSQL,INSERT DEFAULT VALUESatravessa o parser e só falha em restriçãoNOT NULL— logo a recusa é da forma nua, não doMERGE; no MySQL, umMERGEperfeitamente válido recebe o mesmoERROR 1064— logo ali a recusa é da instrução, não da forma. Saída bruta edocker runemTest Delphi\Common_tests\test.merge.mssql.sql.O
MERGEsem nenhuma cláusulaWHENé a instrução pela metade: o cabeçalho sozinho não éMERGEem motor nenhum. Medido antes de decidir a forma, e nenhum dos que têmMERGEaceita: SQL Server 2022Msg 102, Incorrect syntax near ';'; Oracle Free 23ORA-02000: missing WHEN keyword; PostgreSQL 16.14syntax error at or near ";"; Firebird 5.0.4-104 Unexpected end of command. Controle acompanhando: o mesmo texto comWHEN MATCHED THEN UPDATE SET D = 'z'é aceito pelos quatro — logo a recusa é da forma semWHEN, e não doMERGE. A guarda ficou no núcleo (TFluentSQLMerge.Serialize,FluentSQL.Merge.pas:296) e não no serializador do MSSQL, porque a regra é da instrução e não do dialeto.Consequência de ordem, declarada de propósito: como essa guarda roda antes do despacho por dialeto, um
MERGEsemWHENmontado sobre um dialeto relacional sem serializador recebeEArgumentExceptione nãoEFluentSQLStatementNotSupported. As duas seriam verdadeiras; a escolhida é a que aponta a linha que o consumidor escreveu.Medido, dialeto por dialeto, sem
definesextras: Firebird, MSSQL, MySQL, SQLite, Oracle e PostgreSQL →EArgumentException; InterBase e DB2 (desligados no.inc) →EFluentSQLDriverNotRegistered, porque morrem antes, ainda emQuery(); MongoDB → não levanta nada, e devolve{}. A enumeração fecha aqui de propósito, sem reticências: o MongoDB é contraexemplo, não caso omisso.TFluentSQLSerializeMongoDBsobrescreveAsStringinteiro, entãoTFluentSQLMerge.Serialize— onde a guarda mora — nunca chega a rodar. Isso está registrado em Known issues como oMERGEque o MongoDB descarta em silêncio, e travado porTestMerge_MongoDB_DropsMergeSilently_KnownGap.O caso do
nil— e o dos quatro irmãos dele — é de natureza diferente e pior: não emitia SQL inválido, emitia SQL válido com o dado errado.nilnumarray of constchega comovtPointere era convertido porIntToHex, então a coluna recebia a string'00000000'. Pela mesma porta passavam instância (vtObject), referência de classe (vtClass) eUnassigned, convertidos emClassName,ClassNamee string vazia — corrupção silenciosa, sem erro em lugar nenhum, nos quatro.Nullvariante já levantava, masEVariantTypeCastErrorda RTL. O par nome/valor não tem como exprimirNULL, e dar essa semântica anil/Null/Unassignedé decisão de convenção que não foi tomada aqui.O que fazer: passe ao menos um par
('COLUNA', valor); use.Deletese a ação pretendida era outra; e omita a coluna da lista se ela deve ficarNULL. Se o seu código chamava.Update/.Insertsem argumentos, ele estava gerando SQL que o motor rejeitava — a exceção agora aponta a linha.BREAKING CHANGE (comportamento) —
MERGEem dialeto sem serializador passou deEStackOverflowparaEFluentSQLStatementNotSupported. Afeta Firebird, MySQL, SQLite, Oracle e PostgreSQL.TFluentSQLSerialize.Mergeestava escrito como despachante e redelegava ao próprio dialeto, reentrando em si mesmo indefinidamente — não era erro tratável, era estouro de pilha que matava o processo. Só o MSSQL sobrescreveMerge.Quem captura
EStackOverflowpara tratar isso deve trocar porEFluentSQLStatementNotSupported. É a mesma régua aplicada aoEFluentSQLDriverNotRegisteredacima: troca de classe de exceção que pede revisão detry..excepté BREAKING.dbnMongoDBnão mudou e continua sendo lacuna conhecida — ver Known issues.InterBase (
dbnInterbase, desligado por omissão) —LengtheCeilpassaram a levantarEFluentSQLFunctionNotSupportedem vez de emitirLENGTH(...)/CEIL(...). O InterBase divergiu do tronco comum antes de o Firebird 2.1 introduzirCHAR_LENGTHeCEIL/CEILING, e a forma correta para esse dialeto não foi verificada — emitir a forma do Firebird seria repetir o defeito doCEILno MSSQL. Se você liga{$DEFINE INTERBASE}e precisa dessas duas funções, implemente-as emFluentSQL.FunctionsInterbase.pase remova-as da tabela de suporte emTest Delphi\Common_tests\test.driver.functions.matrix.pas.BREAKING CHANGE (classe de exceção, e uma chamada que antes não levantava) — as três guardas de ordem do builder deixaram de ser
Assert. OSource/inteiro tinha exatamente doisAssert, e os dois guardavam a mesma coisa: um método que só faz sentido depois de outro ter sido chamado e que, sem ele, indexaCount-1numa coleção vazia.Asserté removido pelo compilador com{$C-}, que é o default de qualquer build de release — ou seja, em produção não havia guarda nenhuma.Chamada Antes ( {$C+}, debug)Antes ( {$C-}, release)Depois (nas duas) .CaseExpr('T').IfThen('X')semWhenEAssertionFailed:TFluentSQLCriteriaCase.IfThen: Missing WhenEArgumentOutOfRangeException:List index out of bounds (-1). TList<...IFluentSQLCaseWhenThen> is emptyEArgumentExceptionnomeandoIFluentSQLCriteriaCase.IfTheneWhen.CaseExpr('T').ElseIf('X')semWhennão levantava — emitia CASE ELSE X ENDnão levantava — idem EArgumentExceptionnomeandoIFluentSQLCriteriaCase.ElseIf.OrderBy().Descsem colunaEAssertionFailed:TCriteria.Desc: No columns set up yetEArgumentOutOfRangeException:List index out of bounds (-1). TList<...IFluentSQLName> is emptyEArgumentExceptionnomeandoIFluentSQL.DesceORDER BYO
ElseIfé o caso que muda comportamento, não só classe de exceção: ele não tinha guarda alguma e emitia caladoCASE ELSE <valor> END, que nenhum motor aceita — PostgreSQL 16.14ERROR: syntax error at or near "ELSE", SQL Server 2022Msg 156 Incorrect syntax near the keyword 'ELSE', Oracle 26aiORA-00923: FROM keyword not found where expected. Saída bruta emTest Delphi\Common_tests\test.cases.guards.matrix.sql. É a mesma régua já publicada emTUtils._AssertSingleValuee emFluentSQL.Merge.pas:297: entre emitir SQL que o motor recusa e recusar a chamada na linha que a causou, recusa.Quem captura
EAssertionFailed(ou compila com{$C-}e capturaEArgumentOutOfRangeException) para tratar esses três casos deve trocar porEArgumentException. Quem chama na ordem certa não vê diferença:IfThen/ElseIfdepois deWheneDescdepois deOrderBy('COLUNA')continuam emitindo exatamente o mesmo SQL, byte a byte.Detalhe conferido e não corrigido às cegas: o
AssertdoDescafirmava sobreFAST.ASTColumnse indexavaFAST.OrderBy.Columns. As duas são o mesmo objeto enquanto a seção ésecOrderBy— quem as liga é_DefineSectionOrderBy(FluentSQL.pas:794), e_SetSectionnão tem caminho que desfaça a ligação. A afirmação não estava medindo coleção errada na prática; era só o nome errado para ler, e passou a citar a coleção que de fato indexa. Há teste dedicado a essa porta (TestDescComColunaNoSelectMasNenhumaNoOrderByLevanta), porque a equivalência vale enquanto_DefineSectionOrderBya mantiver.Não existe
Ascpara receber a guarda equivalente.IFluentSQLdeclara sófunction Desc: IFluentSQL;(FluentSQL.Interfaces.pas:366);dirAscendingé o default deTOrderByDirection. Varridas as duas famílias noSource/inteiro: na árvore anterior (283512c)Assert(devolvia 2 ocorrências —FluentSQL.Cases.pas:340eFluentSQL.pas:838, as duas acima — e a indexação porCount-1]devolvia 2, exatamente nos mesmos dois métodos (FluentSQL.Cases.pas:342eFluentSQL.pas:839; no HEAD atual,FluentSQL.Cases.pas:658eFluentSQL.pas:865). No HEAD atualAssert(devolve zero noSource/. Todo o resto deCount - 1na biblioteca é limite defor. A família está fechada.(As duas citações desta entrada que apontam para
FluentSQL.Interfaces.paseFluentSQL.Cases.pasforam reabertas e corrigidas na entrega do slot de valor doCASE: eram:315e:379, deslocadas pelo diff daquela entrega. As linhas rotuladas "na árvore anterior (283512c)" continuam corretas contra aquela árvore e não mudaram. As históricas descrevem283512c; as do HEAD descrevem o HEAD.)A fronteira publicada dos 17
array of constnão mudou — recontada, não presumida. (Parágrafo escrito no escopo desta entrada, a das guardas do builder. As frases sobreIfThen/ElseIfabaixo descrevem o que esta entrada fez, e não o estado atual da API: o slot de valor doCASEfoi entregue depois, em Added neste mesmo[Unreleased], e a recontagem definitiva está lá.) O critério mecânico continua sendo "passa porTUtils.SqlArrayOfConstToParameterizedSql", e a contagem por chamada segue em 14 sítios / 17 pontos de entrada (os 4 deIFluentSQLCriteriaExpressioncompartilhamFluentSQL.Expression.pas:224). Esta entrada não acrescentou nem removeu sobrecargaarray of const, e não criou slot de valor — ela mexeu só nas guardas de ordem. Quanto aos "quatro slots de valor (SqlArrayOfConstToParameterizedValue)": o número continua quatro paraarray of const, mas deixou de ser o total da API — ver a recontagem na entrada do slot de valor doCASE.BREAKING CHANGE (API) —
IFluentSQLCriteriaCaseganhou dois métodos:function IfThen(const AValue: Variant; const ADataType: TFluentSQLDataFieldType): IFluentSQLCriteriaCase; overload;efunction ElseIf(const AValue: Variant; const ADataType: TFluentSQLDataFieldType): IFluentSQLCriteriaCase; overload;. Acrescentar método a interface publicada quebra quem a implementa do zero. O que as sobrecargas fazem está em Added; o que está aqui é a quebra de compilação.Impacto real: baixo. No repositório o único implementador é
TFluentSQLCriteriaCase(FluentSQL.Cases.pas), e ele não é descendível de fora com proveito —TFluentSQL.CaseExpr(FluentSQL.pas:338) instancia a classe concreta e não há como injetar outra. Só quebra quem implementaIFluentSQLCriteriaCaseinteira do zero para devolvê-la de umIFluentSQLpróprio: esse código deixa de compilar até declarar os dois métodos (E2291 Missing implementation of interface method).O que fazer: declarar os dois. Se o seu
CASEnão tem como parametrizar valor, o corpo honesto é levantar — e não cair na sobrecarga deString, que interpolaria o valor verbatim e é exatamente o defeito que estas duas existem para fechar.Está aqui e não em Added pela régua que esta mesma lista já aplicou duas vezes — a
IFluentSQLSelectQualifiersquando ganhouRequestsZeroRowse aIFluentSQLFunctionsquando ganhou oCastpor dialeto: "acrescentar método a interface publicada não pode ser rodapé".Nenhum SQL emitido muda por causa desta entrada. As sobrecargas antigas de
IfThen/ElseIf(StringeInt64) têm diff vazio no corpo e continuam interpolando verbatim — há teste de controle para isso (TestSobrecargaDeStringContinuaVerbatim,TestSobrecargaDeInt64ContinuaVerbatim). O que a entrega faz é dar alternativa, não trocar comportamento debaixo de quem já usa.BREAKING CHANGE (API) —
IFluentSQLFunctionsganhou o métodofunction Cast(const AExpression: String; const ADataType: TFluentSQLDataFieldType; const ALength: Integer = 0): String;. Acrescentar método a interface publicada quebra quem a implementa do zero. O que a sobrecarga faz está em Added, junto com a matriz medida que a justifica; o que está aqui é a quebra de compilação.Impacto real: baixo, e provavelmente não é você. No repositório o único implementador é
TFluentSQLFunctionAbstract(FluentSQL.FunctionsAbstract.pas), e os nove drivers descendem dela — quem herda não faz nada. Só quebra quem implementaIFluentSQLFunctionsinteira do zero e a injeta porTFluentSQLRegister.RegisterFunctions: esse código deixa de compilar até declarar o método (E2291 Missing implementation of interface method).O que fazer: passar a descender de
TFluentSQLFunctionAbstract— que já traz a guarda de interseção — ou declarar o método e recusar o que não medir. Ressalva de runtime para quem já descende: o corpo herdado é o abstrato, que levantaEAbstractError; um driver externo que não sobrescrever a sobrecarga nova compila limpo e explode na primeira chamada. É o mesmo custo de qualquer função do padrão B.Está aqui e não em Added pela régua que esta mesma lista já aplicou a
IFluentSQLSelectQualifiersquando ela ganhouRequestsZeroRows: "acrescentar método a interface publicada não pode ser rodapé". A régua não muda de interface para interface.O que NÃO é breaking, para o leitor não confundir: a sobrecarga de enum nasceu nesta mesma branch (commit
059a0e9) e nunca foi publicada. Contramain(33d4391) essa API nunca existiu, então tê-la apertado depois — de oferecer a união das células medidas para oferecer só a interseçãodftString/dftInteger/dftFloat— não quebra ninguém: não havia o que quebrar. O commitd6bbb47traz o rótulorefactor(cast)!por esse aperto, e o rótulo está errado; o histórico não foi reescrito e o erro não se repete aqui. O único BREAKING desta entrega contramainé o membro novo da interface — a sobrecargaCast(String, String)tem corpo com diff vazio e nenhum SQL emitido muda.
Added
Classe de exceção
EFluentSQLConstructNotSupported(FluentSQL.Interfaces.pas), comconstructor Create(const AConstruct, ASaida: String). Irmã deEFluentSQLFunctionNotSupported,EFluentSQLQualifierNotSupportedeEFluentSQLStatementNotSupported, e distinta das três num ponto: aquelas dizem "o seu dialeto não tem", esta diz "não tem em lugar nenhum". Por isso a mensagem não nomeia dialeto — nomear mandaria quem lê tentar outro banco, que é exatamente o caminho errado quando os sete recusam. Aditiva: acrescentar classe não quebra ninguém. Primeira usuária: a guarda deDELETEmulti-relação (ver Changed).O
CASEganhou SLOT DE VALOR:IfThen(AValue: Variant; ADataType: TFluentSQLDataFieldType)eElseIf(...)iguais. Até aquiIfThen/ElseIftinham duas sobrecargas —StringeInt64— e as duas são slot de expressão: o argumento vira termo SQL verbatim. A deInt64não é exceção; ela chamaIntToStre cai na deString(FluentSQL.Cases.pas:607paraIfThen,:320paraElseIf). Quem escrevia ali um valor vindo do usuário estava concatenando SQL — a mesma classe de defeito que oMERGEe oSetValue/Valuesfecharam acima nesta mesma lista. Não havia, em sobrecarga nenhuma, como ligar um valor num ramo doCASE.Chamada SQL (Firebird) Parâmetros .IfThen('SEGREDO')— sobrecarga antiga, inalterada... THEN SEGREDO ...0 .IfThen(Int64(42))— sobrecarga antiga, inalterada... THEN 42 ...0 .IfThen('SEGREDO', dftString)— nova... THEN CAST(:p1 AS VARCHAR(4000)) ...1, valor SEGREDO.ElseIf(Int64(42), dftInteger)— nova... ELSE CAST(:p2 AS INTEGER) ...1, valor 42O
CASTnão é enfeite, e é a razão de esta tarefa ter ficado bloqueada. Parâmetro nu em posição deTHEN/ELSEé recusado noPREPAREpor dois dos sete: Firebird 5.0.4 responde-804/HY004 Data type unknown, e DB2 v12.1.5.0 respondeSQL0418N/42610 untyped parameter marker. E não é culpa doCASE: isolado,SELECT :a FROM RDB$DATABASEdá o mesmo-804. É o marcador sem tipo.A forma que esta entrega emite foi medida nos sete, com as strings exatas, e a medição é desta branch — não é citação da T17. Docker engine 29.6.2; versões perguntadas ao motor, não presumidas da tag. Transcrição literal em
Test Delphi\Common_tests\test.cases.bind.matrix.sql, caso F:Motor controle (marcador nu) dftStringdftIntegerdftFloatFirebird 5.0.4 -804/HY004yes202.500000000000000DB2 v12.1.5.0 SQL0418N/42610SQL0313N→ prepare passouidem idem PostgreSQL 16.14 aceita yes202.5MySQL 8.4.11 aceita yes202.5SQLite 3.53.4 aceita yes202.5SQL Server 2022 CU26 aceita yes202.5Oracle 26ai 23.26.2.0.0 aceita yes202.5E+000InterBase não medido — não há imagem pública A coluna do controle é o ponto: é a única com recusa, e o
CASTa converte em aceitação nos dois motores que recusavam. Nas outras cinco oCASTnão era necessário e também não atrapalhou — que era a outra metade da aposta de "emitirCASTnos sete", e até aqui essa metade nunca tinha sido executada. OdftFloatdentro doCASEtambém nunca tinha sido executado em motor nenhum; agora foi, nos sete que têm imagem.O
CASTsai nos sete, e não só nos dois que exigem. Sendo esta uma sobrecarga nova, não há SQL emitido hoje por ela e portanto não há oráculo a quebrar — uniformizar custa zero. A alternativa, uma tabela de "quem precisa de tipo", envelheceria com a versão do motor. É o oposto do que valeu para o apelido de tabela do Oracle, onde emitir a forma nova nos sete teria trocado o texto de seis dialetos que já funcionavam; lá havia oráculo, aqui não há.Dialeto dftStringdftIntegerdftFloatFirebird CAST(:p1 AS VARCHAR(4000))CAST(:p1 AS INTEGER)CAST(:p1 AS DOUBLE PRECISION)SQL Server CAST(:p1 AS NVARCHAR(4000))CAST(:p1 AS INT)CAST(:p1 AS FLOAT)MySQL CAST(? AS CHAR)CAST(? AS SIGNED)CAST(? AS DOUBLE)Oracle CAST(:p1 AS VARCHAR2(4000))CAST(:p1 AS INTEGER)CAST(:p1 AS BINARY_DOUBLE)PostgreSQL CAST(:p1 AS VARCHAR)CAST(:p1 AS INTEGER)CAST(:p1 AS DOUBLE PRECISION)SQLite CAST(:p1 AS TEXT)CAST(:p1 AS INTEGER)CAST(:p1 AS REAL)DB2 CAST(:p1 AS VARCHAR)CAST(:p1 AS INTEGER)CAST(:p1 AS DOUBLE)O
?do MySQL é o serializador do próprio driver reescrevendo:pN(FluentSQL.SerializeMySQL.pas:52), não outra grafia deCAST. A largura não é decidida noCASE:ALengthfica no default e cada driver resolve — daíVARCHAR(4000)no Firebird (que exige largura) eVARCHARsem largura no DB2 (onde impor 4000 criaria um teto que o motor não tem:CAST(REPEAT('x',300) AS VARCHAR)devolve 300).ADataTypeaceita sódftString,dftIntegeredftFloat— o mesmo conjunto doCastportável, pela mesma porta (TFluentSQLFunctionAbstract._AssertCastTypeIsPortable), e não por uma segunda lista. Os outros 7 membros do enum levantamEFluentSQLFunctionNotSupported, uniformemente. Consequência herdada dessa porta única, e declarada: em MongoDB e InterBase oCastportável levanta, logo este slot levanta neles — erro nomeado, não MQL indefensável nem grafia inventada.⚠️ O
CASTé portável na SINTAXE, não na SEMÂNTICA — e nada aqui validaAValuecontraADataType. No uso correto (o valor é do tipo que você declarou) oCASTé inofensivo, e no SQLite chega a ser identidade. Mas se os dois discordarem, os sete divergem, e a pior metade diverge em silêncio:IfThen('BANANA', dftInteger)vira0calado no SQLite e no MySQL (regras de afinidade e de conversão, não erro), e erro de motor nos outros. É a mesma divergência que a matriz da T17 já documenta paraCast(x, dftDate)no SQLite, agora alcançável por este slot. Declare-se o que é: o framework garante que o valor vai ligado e que a grafia doCASTé a do dialeto; ele não garante que o valor cabe no tipo. Guarda de valor-contra-tipo é decisão de desenho e não foi tomada aqui.NulleUnassignedsão recusados comEArgumentExceptionnomeando a chamada. Não é limitação técnica:CASE WHEN c THEN NULL ENDé SQL legítimo e o parâmetro carregaVariant. É que dar semântica deNULLa este slot é decisão de convenção, e ela não foi tomada aqui — e a assimetria manda recusar: aceitar depois é aditivo, recusar depois seria BREAKING. É a mesma régua já escrita emTUtils._AssertValueSlotCarriesData.nilnão chega a ser questão de runtime: não compila. O erro dodcc3236.0, medido, éE2250 There is no overloaded version of 'IfThen' that can be called with these arguments— e nãoE2010:IfThené sobrecarregado, então a resolução de sobrecarga responde antes da compatibilidade de tipos. (OE2010 Incompatible types: 'Variant' and 'Pointer'sai da atribuição diretaV := nil, que é outro experimento e não esta chamada.) A decisão "nillevanta, não viraNULL" está cumprida pelo sistema de tipos nesta sobrecarga.O INVARIANTE, e ele custou uma rodada de review inteira: nenhum caminho de recusa pode deixar parâmetro para trás, e nenhum termo que carrega
:pNpode ser substituído. São cinco portas de recusa, e quatro delas chegaram a vazar numa versão anterior desta entrega:# Causa da recusa Guarda Vazava? 1 sem When_AssertHaveWhennão 2 ramo já ocupado por um slot de valor _AssertValueSlotFreesim, sem exceção nenhuma 3 Null/Unassigned_AssertValueCarriesDatanão 4 tipo fora da interseção a sonda não 5 dialeto sem grafia de CASTa sonda (mesma linha) sim A causa 5 é a que quase passou: em
InterBase— que é relacional, não é o caso do driver não relacional — oCastlevanta de dentro da própria chamada, ou seja depois deParams.Addjá ter corrido. Medido:Params.Count = 1, com o dado do usuário na coleção e nenhum SQL que o citasse. O controle (causa 4, no PostgreSQL) dava0— o padrão existia em todas as células menos naquela.Fechada com uma sonda:
_ValueSlotTermchama o mesmoCastcom uma expressão descartável antes de gravar, e só grava se ele não recusar. A sonda fecha a causa 4 junto com a 5, porque_AssertCastTypeIsPortableé a primeira linha deTFluentSQLFunctions.Cast— provocar oCastprovoca as duas. Isso é medição, não elegância: uma versão anterior desta entrega chamava_AssertCastTypeIsPortableexplicitamente ali (e alargava a visibilidade dele parapublicsó por isso); com a sonda no lugar, mover essa chamada para depois doParams.Addnão deixava um único teste vermelho — era linha morta. Ela saiu, e com ela a alteração de visibilidade: a política da T17 fica onde a T17 a pôs,protected, e esta entrega não mexe emFluentSQL.FunctionsAbstract.pasa não ser em comentário.A sonda só é equivalente porque a recusa do
Castdepende de (dialeto, tipo) e não da expressão — conferido lendo os nove drivers, e travado por teste que compara a recusa da sonda com a da chamada real, para o dia em que um driver divergir.A causa 2 nem envolve exceção no caminho que a motivou. Chamar
IfThenduas vezes no mesmo ramo substituía o texto e abandonava o:pNda primeira chamada — medido, em Firebird:Sequência SQL emitido Coleção .IfThen('A',dftString).IfThen('B',dftString)THEN CAST(:p2 ...)p1=A, p2=Bp1órfão.IfThen('A',...).ElseIf('B',...).ElseIf('C',...)THEN CAST(:p1) ELSE CAST(:p3)p1,p2,p3p2órfão.IfThen('A',dftString).IfThen('''LITERAL''')THEN 'LITERAL'p1=Ap1órfão, e o dado do usuário fica na coleção fora do SQLQuem liga parâmetro por posição — como todo driver Delphi liga — liga errado a partir do primeiro buraco. Substituir um ramo que já carrega
:pNpassou a ser recusado, e a guarda vale também nas sobrecargas deStringeInt64, porque a substituição que vaza pode vir delas (é a terceira linha da tabela). Isso não quebra código anterior a esta entrega: para o slot estar ocupado alguém tem de ter chamado a sobrecarga deVariant, que nasceu aqui — programa que não a chama não alcança a recusa.StringsobreStringcontinua permitido e continua não vazando, e há controle para isso. O que libera o slot é umWhennovo, porque cadaWHENtem o seu próprio ramoTHEN.Toda célula de recusa da suíte confere a coleção, e não só a classe da exceção — na forma
Delta = 0("a chamada recusada não acrescenta parâmetro"), que é o que vale nas cinco causas.Total = 0seria errado na causa 2, onde a primeira chamada ligou um:p1que o SQL cita: exigir zero ali seria exigir que um parâmetro referenciado fosse apagado. Há ainda a asserção na forma mais pura, em célula própria: todo parâmetro da coleção tem de ser citado pelo statement — essa sozinha teria pego os quatro vazamentos.RECONTAGEM DA FRONTEIRA — o que esta entrega mediu, e o que ela deliberadamente NÃO publica.
Os
array of constem posição de expressão continuam 17, e este é o comando que reproduz. No HEAD desta entrega:grep -rn "TUtils\.SqlArrayOfConstToParameterizedSql(" Source/ --include=*.pas \ | grep -v "^Source/Core/FluentSQL.Utils.pas" | wc -l→ 14 sítios de chamada. (Sem o segundo filtro o
grepdevolve 15; o 15.º é a própria implementação,FluentSQL.Utils.pas:287, e não uma chamada — é a armadilha de contar a definição junto com os usos.) ComoFluentSQL.Expression.pas:224serve 4 pontos de entrada (AndOpe,OrOpe,Ope,Fun), são 14 sítios / 17 pontos de entrada.Esta entrega não acrescentou nem removeu sobrecarga
array of const— as duas novas recebemVariant. Os números 14/17 estão inalterados; o que mudou foram três citações da tabela acima, porqueFluentSQL.Cases.pascresceu (When383→662,AndOpe268→285,OrOpe354→599).O que a T13 mudou na fronteira de quem LIGA valor: acrescentou 2 pontos de entrada —
IfThen(Variant, TFluentSQLDataFieldType)eElseIf(Variant, TFluentSQLDataFieldType), que chamamIFluentSQLParams.Adddireto (FluentSQL.Cases.pas:589). É só isso que esta entrada afirma sobre essa fronteira, e a omissão de um total é deliberada.Não se publica aqui um total de "pontos de entrada que ligam valor", porque medir mostrou que todo total curto que se tentou escrever estava errado. A varredura completa —
grep -rn "Params\.Add(" Source/ --include=*.pas | grep -v "function .*Add(const AValue" | wc -l→ 29 sítios, e eles encontram binding direto em lugares que nada têm a ver com
array of conste já existiam emmain. A itemização fecha com o total — confira somando:Arquivo Sítios O que são Linhas FluentSQL.pas8 as sobrecargas tipadas de SetValue(String,Integer,Extended,Double,Currency,TDate,TDateTime,TGUID)412, 440, 451, 462, 473, 484, 495, 506FluentSQL.Operators.pas8 o lado direito de Equal/Like/Ine afins174, 176, 178, 180, 187, 189, 297, 317FluentSQL.Utils.pas12 o interior dos próprios helpers de array of const— não são pontos de entrada90, 95, 100, 105, 110, 119, 124, 129, 134, 144, 149, 245FluentSQL.Cases.pas1 o CASEdesta entrega58929 Duas afirmações que uma versão anterior desta entrada chegou a publicar e que a medição desmentiu:
- o total "6 pontos de entrada que ligam valor" era subcontagem — ignorava as 8 tipadas de
SetValuee os 8 dos operadores; IFluentSQLParams.Adddireto NÃO é "um terceiro caminho que a T13 criou". Ele já era o caminho das sobrecargas tipadas e dos operadores emmain. A T13 acrescentou dois usos dele; não inaugurou nada. Dar o caminho como novidade desta entrega seria creditá-la por algo que ela não fez.
E o critério que se tentou publicar — "se a assinatura traz um
TFluentSQLDataFieldTypeao lado do valor" — também não classifica: nenhuma das 8 sobrecargas tipadas deSetValuetem esse parâmetro, e todas ligam valor.A regra que sobrevive à medição, e a única publicada aqui: dentro de
array of const,SqlArrayOfConstToParameterizedSql→ literal,SqlArrayOfConstToParameterizedValue/NameValuePairs→ parâmetro. Fora dearray of constnão há atalho — quem quiser o conjunto completo roda ogrepdeParams.Add(acima e lê os 29.- o total "6 pontos de entrada que ligam valor" era subcontagem — ignorava as 8 tipadas de
IFluentSQLFunctions.Castganhou sobrecarga por dialeto:Cast(const AExpression: String; const ADataType: TFluentSQLDataFieldType; const ALength: Integer = 0).Castera a última função escalar no padrão A — o núcleo emitia'CAST(' + AExpression + ' AS ' + ADataType + ')'sem consultar o driver, e nenhum dos nove drivers a sobrescrevia. Uma grafia respondia pelos sete dialetos. É o mesmo defeito estrutural que já queimouCEIL/LENGTH, com um agravante: aqui a célula errada frequentemente não levanta.A matriz 10 tipos × 7 dialetos foi medida contra motor real —
Test Delphi\Common_tests\test.cast.matrix.sqltrazdocker run, versão e transcrição literal dos erros. Das 70 células, 46 existem e 24 não. Nenhum dialeto tem as 10; sódftString,dftIntegeredftFloatexistem nos sete — e mesmodftStringsai em seis grafias distintas, sob duas políticas de largura opostas:Firebird 5.0.4 SQL Server 2022 MySQL 8.4.11 SQLite 3.53.4 Oracle 26ai PostgreSQL 16.14 DB2 12.1.5.0 dftStringVARCHAR(4000)NVARCHAR(4000)CHARTEXTVARCHAR2(4000)VARCHARVARCHARdftIntegerINTEGERINTSIGNEDINTEGERINTEGERINTEGERINTEGERdftFloatDOUBLE PRECISIONFLOATDOUBLEREALBINARY_DOUBLEDOUBLE PRECISIONDOUBLEdftDateDATEDATEDATE— DATEDATEDATEdftTextBLOB SUB_TYPE TEXTNVARCHAR(MAX)CHARTEXT— TEXTCLOBdftDateTimeTIMESTAMPDATETIMEDATETIME— TIMESTAMPTIMESTAMPTIMESTAMPdftGuid— UNIQUEIDENTIFIER— — — UUID— dftBooleanBOOLEANBIT— — BOOLEANBOOLEANBOOLEANdftArray/dftUnknown— — — — — — — A tabela acima é o retrato dos MOTORES, e não a lista do que a sobrecarga oferece — as duas não coincidem, e a diferença é a decisão desta entrega.
Cast(x, ADataType: TFluentSQLDataFieldType)é a sobrecarga portável, e portável quer dizer interseção, não união: ela aceita exclusivamentedftString,dftIntegeredftFloat, os três que existem nos sete. Os outros sete membros do enum levantamEFluentSQLFunctionNotSupportedem todos os dialetos, com a mesma mensagem — inclusive naquele em que a célula existe e foi medida:Tipo recusado Existe em Mas é negado por Motivo medido dftDate6 dos 7 SQLite CAST('2026-08-10' AS DATE)devolve2026, sem errodftDateTime6 dos 7 SQLite idem, devolve 2026dftText6 dos 7 Oracle ORA-22849:CLOBnão é alvo deCAST(eERROR 1064no MySQL)dftBoolean5 dos 7 MySQL ERROR 1064; e no SQLite devolve0calado; e na Oracle depende da versão do servidordftGuid2 dos 7 Firebird -607;ORA-01465na Oracle; sem tipo em MySQL/SQLite/DB2dftArray0 dos 7 todos nenhum motor aceita ARRAYcomo alvo deCASTdftUnknown— — não é tipo em lugar nenhum Por que uniforme e não célula a célula.
dftGuidrespondeUUIDno PostgreSQL eUNIQUEIDENTIFIERno SQL Server; oferecê-lo só ali faria a mesma chamada responder em dois motores e levantar em cinco. Isso não é "uma API portável com lacunas" — é uma API que depende do banco, e o programador só descobriria na migração, que é o momento mais caro possível.dftBooleanfecha o argumento sozinho: na Oracle a célula só vale em 23ai+, e o FluentSQL não tem como saber a versão do servidor — uma célula cuja validade depende de informação que a biblioteca não possui não é promessa, é palpite.Alargar a lista depois é aditivo e barato; estreitá-la depois seria
BREAKING. Por isso começa apertada, enquanto nada depende dela. A lista canônica écFluentSQLCastPortableTypes(FluentSQL.Interfaces.pas), e a matriz medida não foi jogada fora quando a política encolheu para três: ela é a justificativa da política e a fonte para quem for escrever a grafia à mão.Três achados sustentam cada
—da tabela, e os três foram medidos:- A palavra sozinha não é o tipo inteiro, e o erro é silencioso.
CAST(x AS NVARCHAR)sem(n)no SQL Server trunca calado em 30: 40 caracteres entram, 30 saem, sem erro nem aviso. Um mapeamento ingênuodftString → 'NVARCHAR'trocaria uma incoerência de API por corrupção silenciosa de dado. No Firebird (-104) e na Oracle (ORA-00906) a mesma forma é erro de sintaxe, e no PostgreSQL a largura é que introduz truncamento (VARCHAR(4)sobre 10 caracteres devolve 4, calado). Por isso a largura é decisão por driver, não carimbo do núcleo. - A grafia ANSI é erro de sintaxe no MySQL. O alvo de
CASTno MySQL é lista fechada na gramática:INTEGER,TEXT,BOOLEANeUUIDdãoERROR 1064. O núcleo emitiaCAST(x AS INTEGER)para todos, e o próprio teste da casa exercitava essa string dando-a por boa nos sete —test.driver.functions.matrix.pasainda a exercita, de propósito, porque ali o que se mede é a sobrecarga deString; ficou escrito no cabeçalho e ao lado da chamada que aquela célula verde afirma que o escape hatch responde, e não que o SQL roda. - O SQLite nunca recusa, e destrói o dado. Qualquer palavra é aceita e resolvida por afinidade — inclusive
BANANA.CAST('2026-08-10' AS DATE)devolve2026;CAST('true' AS BOOLEAN)devolve0;CAST('6F9619FF-…' AS UUID)devolve6. Nenhuma levanta. É o único modo de falha pior que emitir SQL inválido, e é este dialeto que decidiu a política: se a sobrecarga oferecesse a união, o mesmoCast(x, dftDate)que roda certo no PostgreSQL chegaria aqui e devolveria2026— bug que não aparece em teste, aparece em relatório, meses depois.
A largura default é
cFluentSQLCastDefaultLength = 4000, e não é número redondo escolhido a esmo: é o maior valor simultaneamente legal nos dois motores mais restritivos —VARCHAR2(4001)dáORA-00910eNVARCHAR(4001)dáMsg 131 … exceeds the maximum allowed for any data type (4000). Está com folga sob o teto do Firebird em UTF8 (8191, medido). Quem precisar de outra passaALengthexplícito.Recusar não é bloquear: há duas portas de escape, e as duas dizem ao leitor que a portabilidade passou a ser dele.
Porta Forma De quem é a garantia Sobrecarga de StringCast('C', 'UNIQUEIDENTIFIER')sua — emite verbatim o que você escrever ForDialectOnlyregistra o fragmento só no motor em que ele vale sua, e a escolha fica visível na linha Sobrecarga de enum Cast('C', dftString)do framework — mesma garantia nos dialetos relacionais, que são o alcance da promessa A terceira linha diz relacionais e é literal: a garantia da sobrecarga de enum é a interseção dos dialetos relacionais (os 6 ligados por omissão mais o DB2, desligado no
.inc), que é o conjunto medido na matriz acima. Num build padrão odbnMongoDBtambém está registrado e não está sob essa promessa —Func(dbnMongoDB).Cast('c', dftString)levantaEFluentSQLFunctionNotSupported, como levanta para qualquer escalar naquele driver.A sobrecarga
Cast(String, String)permanece, inalterada e no padrão A. Nenhum SQL hoje emitido muda — o corpo dela tem diff vazio, e há teste fixando que ela continua verbatim nos nove dialetos. Ela é a porta de escape e a armadilha:Cast(x, 'INTEGER'), a grafia que qualquer um escreve por reflexo e que o núcleo emitia para os sete até esta entrega, éERROR 1064no MySQL. Quem usa a sobrecarga deStringassume esse risco por contrato; quem usadftIntegerrecebeSIGNEDe não precisa saber que a lista existe. A quebra de compatibilidade que esta sobrecarga traz não é rodapé desta entrada:IFluentSQLFunctionsganhou membro, e isso tem entrada própria comoBREAKING CHANGE (API)em Changed.A recusa mora num lugar só, e isso é a política, não economia de linha.
TFluentSQLFunctionAbstract._AssertCastTypeIsPortableé chamada como primeira linha dos dezoverridedeCast— os nove drivers e o núcleo. Se cada driver escrevesse a própria, a mensagem derivaria e o erro viraria pista de qual dialeto está ligado: o programador leria "o MySQL não suporta" e concluiria, errado, que trocar de banco resolve. Em InterBase, que recusa tudo com mensagem própria de dialeto, a guarda vem antes doraisepróprio — são duas causas diferentes (política de interseção × driver não medido) e a mensagem tem de dizer qual é.InterBase levanta nas dez células, de propósito: não há imagem pública do motor, nenhuma célula foi medida, e a grafia não foi inferida do Firebird. Esta própria matriz mostra por que a recusa não é preciosismo — Oracle e DB2 divergem em
CLOB, Firebird e DB2 divergem na obrigatoriedade da largura. (Com a interseção estrita, medir o InterBase um dia passou a custar três células, não dez.)Isto desbloqueou o slot de valor do
CASE. Firebird e DB2 recusavamCASE WHEN c THEN :p1 ELSE :p2 ENDnoPREPARE(-804 Data type unknown/SQL0418N untyped parameter marker); medido com as strings exatas que estes drivers emitem, os dois passam doPREPARE:CAST(:p1 AS VARCHAR(4000))no Firebird eCAST(? AS VARCHAR)no DB2 — no DB2 oSQL0313Nque responde é o CLP pedindo valores, ou seja oPREPAREpassou. As duas strings estão travadas por teste.Desambiguação, porque as duas entregas convivem neste mesmo
[Unreleased]: o slot de valor não foi implementado nesta entrada — a doCast, que só removeu o bloqueio. Ele foi implementado na entrada própria acima (O CASE ganhou SLOT DE VALOR), e existe. Nenhuma frase deste bloco deve ser lida como "o slot não existe".- A palavra sozinha não é o tipo inteiro, e o erro é silencioso.
Matriz
Test Delphi\Common_tests\test.cast.matrix.pas(9 testes, ligada emTestFluentSQL_Common.dpr), declarando o texto literal esperado de cada célula — não "não levanta", mas a grafia exata. A comparação é case-sensitive de propósito (Assert.AreEqual(..., False, ...)), porque o default do DUnitX ignora caixa e deixarianvarcharpassar porNVARCHAR. Três testes travam a política, e não a grafia:TestRecusaDeTipoForaDaIntersecaoEUniforme— para cada tipo fora da interseção, exige que todo dialeto registrado levante e que a mensagem seja idêntica caractere a caractere. É a segunda metade que tem dentes: mensagem por dialeto significa que a guarda deixou de ser a primeira linha doCastnaquele ponto.TestListaPortavelEExatamenteOsTresUniversais— a lista é exatamentedftString,dftInteger,dftFloat, eDataFieldTypeNameé conferido membro a membro (a tabela é posicional: acrescentar membro quebra a compilação comE2072, mas reordenar o enum não quebra nada — só faz a mensagem nomear o tipo errado, calada).TestMensagemDeRecusaEnsinaASaida— a mensagem nomeia o tipo pedido, diz o que a sobrecarga aceita, diz que a recusa é de interseção e não do banco do leitor, e entrega as duas saídas com a grafia pronta.
O teste varre as DUAS portas que implementam
IFluentSQLFunctions—TFluentSQLFunctions(a do núcleo, queTFluentSQLusa) eTFluentSQLRegister.Functions(D)(o objeto do dialeto). A guarda do núcleo faz curto-circuito, então por aquela porta oCastdo driver nunca chega a rodar para tipo fora da interseção — sem varrer a porta do driver, as nove guardas de driver seriam decoração não observável: medido, com o teste batendo só no núcleo, fazer o PostgreSQL responderUUIDparadftGuidpassava verde. Varrendo as duas, essa mutação fica vermelha.E a metade simétrica, que é preciso dizer porque é pior: a guarda do núcleo também é removível sem uma linha vermelha. Apagar
_AssertCastTypeIsPortable(ADataType);deTFluentSQLFunctions.Cast(String, TFluentSQLDataFieldType, Integer)deixa a suíte inteira verde nas duas configurações, porque o driver logo abaixo recusa com a mesma mensagem e nenhum teste consegue distinguir de onde ela veio. Nenhuma das duas camadas é dispensável, e as duas ficam:- sem a do núcleo, a garantia passa a depender de todo driver futuro lembrar de chamar a guarda — quem esquecer reintroduz a união em silêncio, que é o modo de falha que esta política existe para matar. E é só esta camada que defende o cenário do
IFluentSQLFunctionsde terceiro:RegisterFunctionsfazAddOrSetValue(FluentSQL.Register.pas:249), ou seja substitui o nosso objeto, e com ele vai embora a guarda do nosso driver. Medido com um terceiro registrado emdbnPostgreSQL: pela porta do núcleo a chamada é barrada; pela porta do driver ela chega ao terceiro. E um terceiro registrado não é sequer alcançável pela API fluente —FunceQueryconstroem cada um o seu próprioTFluentSQLRegister(FluentSQL.pas:267eFluentSQL.pas:694) e não há construtor nem propriedade para injetar um de fora; - sem as dos drivers, a garantia passa a depender de o consumidor entrar pela porta do núcleo — e a porta do driver é pública:
TFluentSQLRegister.Functions(D).Cast(x, dftGuid)sobre os nossos drivers é caminho real e alcançável, e é a guarda do driver que o barra (medido). Essa razão basta sozinha. O que não se pode escrever aqui é que a biblioteca use essa porta como entrada independente: as únicas chamadas aTFluentSQLRegister.Functions(D)noSource/estão no despacho deFluentSQL.Functions.pas, sempre depois da guarda do núcleo.
Manter as duas custa uma linha por driver, e a mensagem tem fonte única em
TFluentSQLFunctionAbstract._AssertCastTypeIsPortable, então não pode derivar. A redundância é deliberada e está declarada em comentário na própria linha (Source/Core/FluentSQL.Functions.pas, sobreTFluentSQLFunctions.Cast(String, TFluentSQLDataFieldType, Integer)). Não há teste que a cubra, e não foi inventado nenhum: um teste que só existisse para dar cor à camada redundante seria decorativo — prefere-se a lacuna declarada.Os pisos de contagem dos dois testes de matriz contam apenas os dialetos RELACIONAIS (60 células e 84 recusas, com 6 relacionais ligados por omissão). A promessa de interseção é relacional; um piso que se apoiasse num dialeto fora dela afirmaria sobre os dialetos vivos contando com um que não vota. Os demais dialetos registrados continuam sendo varridos célula a célula, com a mesma exigência de texto — como extra, sem sustentar número.
EFluentSQLDriverNotRegisteredeEFluentSQLFunctionNotSupportedemFluentSQL.Interfaces.pas, para que falhas de dialeto sejam tratáveis pelo consumidor em vez deEAccessViolation/EAbstractError.EFluentSQLQualifierNotSupportedemFluentSQL.Interfaces.pas. Substitui oito cópias deraise Exception.Create('... Unknown qualifier')— quatro delas nomeando o driver errado na mensagem.Matriz
Test Delphi\Common_tests\test.builder.guards.pas(11 testes, ligada emTestFluentSQL_Common.dpr) fixando as três guardas de ordem do builder que eramAssertou não existiam —IfThen,ElseIfeDesc, cada uma com célula para "levanta" e célula para "a mensagem nomeia a chamada", mais dois controles de que o caminho legítimo continua emitindo o mesmo SQL. Os testes rodam idênticos com{$C+}e com-$C-, porque a exceção esperada éEArgumentException, que nenhumAssertproduz.Oráculos de motor real em
Test Delphi\Common_tests\:test.cases.bind.matrix.sql(parâmetro em posição deTHEN/ELSEnos 7 dialetos — a medição que sustenta a lacuna declarada em *Known issues*) etest.cases.guards.matrix.sql(CASEsem nenhumWHEN, recusado por PostgreSQL, SQL Server e Oracle). Trazemdocker run, versão do motor, a forma exata em que cada cliente prepara o parâmetro, e a saída bruta transcrita. O sufixo é.matrixe não um dialeto porque a afirmação só existe comparando os motores entre si.Oráculos de paginação em motor real, um por dialeto, em
Test Delphi\Common_tests\:test.pagination.{mssql,oracle,firebird,sqlite,mysql,postgresql}.sqletest.pagination.mongodb.js. Trazem odocker runexato, a versão do motor e a saída bruta transcrita. Motores medidos: SQL Server 2022 (16.0.4265.3), Oracle Free 23.26.2.0.0, Firebird 5.0.4, MySQL 8.4.11, PostgreSQL 16.14, MongoDB 7.0.39, SQLite 3.50.4 (biblioteca embutida no CPython 3.14, módulosqlite3).Duas versões de SQLite aparecem neste CHANGELOG e as duas são medidas, não erro de digitação: os oráculos de paginação usaram a biblioteca embutida no CPython 3.14 (3.50.4); o oráculo de
MERGEusou a imagemkeinos/sqlite3:latest(3.53.4). Conferido nesta rodada:docker run --rm -i keinos/sqlite3:latest sqlite3 :memory: "select sqlite_version();"→3.53.4;python -c "import sqlite3; print(sqlite3.sqlite_version)"→3.50.4. Nenhuma conclusão desta entrega depende da diferença — o SQLite não temMERGEem nenhuma das duas.Matriz de teste
Test Delphi\Common_tests\test.pagination.filter.pasfoi de 15 para 28 testes, e a contagem merece o detalhe porque+13esconde o que aconteceu: 6 removidos, 19 acrescentados. Os 6 removidos descreviam a janelaROW_NUMBER()que deixou de existir, e cada um tem substituto:Removido Substituto TestMSSQLPaginacaoComFiltroEmiteWhereNaoAndTestMSSQLPaginacaoComFiltroPreservaPredicadoTestMSSQLPaginacaoSemFiltroContinuaComWhereTestSqlExatoMSSQL+TestFirstSozinhoEmTodoDialetoTestMSSQLFirstSozinhoNaoInventaLimiteInferiorTestFirstSozinhoEmTodoDialeto(7 dialetos, não 1)TestMSSQLSkipSozinhoNaoInventaLimiteSuperiorTestSkipSozinhoEmTodoDialeto+TestSqlExatoSkipSozinhoTestMSSQLJanelaUsaOrderByDoUsuarioTestMSSQLOrderByDoUsuarioPrecedeOffsetFetchTestMSSQLJanelaSemOrderByUsaSelectNullTestMSSQLSemOrderByUsaSelectNullNenhuma regra deixou de ser verificada; três delas passaram de 1 para 7 dialetos.
Implementações em falta de
Trim,LTrim,RTrim,Coalesce,Modulus,CurrentDateeCurrentTimestampnos drivers Firebird, SQLite e Oracle.Funções escalares do MongoDB passam a levantar
EFluentSQLFunctionNotSupportedem vez deEAbstractError. As agregações (Count,Sum,Min,Max,Average) não mudaram.EFluentSQLStatementNotSupportedemFluentSQL.Interfaces.pas. Irmão deEFluentSQLFunctionNotSupportedum nível acima: lá falta uma função escalar, aqui falta a instrução inteira no dialeto. Primeiro uso:MERGE, que só o MSSQL serializa.DriverName(ADriver: TFluentSQLDriver): StringemFluentSQL.Interfaces.pas. Nome canônico do dialeto, agora fonte única: é a chave dos dicionários doFluentSQL.Register.pase o texto das mensagens de exceção. Antes existia apenas comoconstprivadaTStrDBEngineNamedentro doRegister, o que impedia qualquer outra unit de nomear um dialeto sem duplicar a tabela. ORegisterpassou a chamá-la nos 19 pontos onde indexava aconst; as chaves dos dicionários são as mesmas strings, sem mudança de comportamento. A proteçãoE2072contra membro novo do enum foi junto com a tabela.Oráculo de motor real
Test Delphi\Common_tests\test.merge.mssql.sql, medindo a injeção viaMERGEantes e depois, em SQL Server 2022 (RTM-CU26) 16.0.4265.3. Traz odocker runexato, o SQL emitido pela própria biblioteca nas duas árvores, a saída bruta do motor e a seção FRONTEIRA, com o que a correção não fecha.Matriz
Test Delphi\Common_tests\test.merge.matrix.pas(9 testes): uma célula por dialeto, afirmando parametrização onde há serializador e exceção nomeada onde não há.Test Delphi\MSSQL_tests\test.merge.mssql.pasfoi de 5 para 34 testes — inclui 16 casos de payload hostil (aspa simples, aspa dupla,;,--,/* */, aspa dobrada,UNION), oO'Brienque tem de funcionar, as guardas de lista malformada / vazia / sem argumentos, onilque não pode virar'00000000', e a asserção de que a cláusula recusada não sobra pela metade no SQL.Três dos cinco testes originais desse arquivo passavam sobre SQL inválido: usavam
.Update/.Insertsem argumentos e asseveravam comPos()sobre prefixos —'WHEN MATCHED THEN UPDATE'casa emUPDATE SET ;tão bem quanto emUPDATE SET [X] = :p1. Foi essa fragilidade que deixou uma forma quebrada atravessar a suíte verde inteira e chegar ao guia como recomendação. Os três passaram a usar a forma válida e a asseverar o SQL inteiro, comAssert.AreEqualcase-sensitive — o padrão do DUnitX é case-insensitive, o que numa biblioteca cujo produto é texto SQL deixa passar regressão de caixa.Oráculo de motor real
Test Delphi\Common_tests\test.setvalue.mssql.sql, para o caminho deSetValue/Values(INSERT/UPDATEcomum, semMERGE), medido em SQL Server 2022 (RTM-CU26) 16.0.4265.3. Registra os quatro casos antes/depois — inclusive um resultado que corrige uma suposição: o payload com aspa não derruba a tabela nessa gramática, porque a aspa abre um literal que engole o resto do batch; o que executa é o que fecha o parêntese (1); DROP TABLE USERS; --).Onze testes em
Test Delphi\Common_tests\test.core.params.paspara o slot de valor deSetValue/Values(array of const)— que até então não tinha teste nenhum. Rodam nos projetos Firebird e MySQL, os dois que compilam esse arquivo. São 3 de parametrização (a string vira:pN, inclusive o payload hostil), 3 donilque levanta, e 5 de cardinalidade (lista vazia emINSERT, emValuese emUPDATE; mais de um valor; e a coluna recusada não sobrar pela metade no SQL).Os 3 do
nilmerecem nota porque o comportamento já existia e mesmo assim não estava coberto: a guarda é o ramovtPointercompartilhado com oMERGE, e medindo por mutação (vtPointer→if False) o vermelho aparecia só em testes deMERGE. O caminho mais trafegado dos dois estava apoiado num oráculo que não o observava.
Fixed
- MSSQL — paginar descartava
UNIONeWITH(CTE) em silêncio.TFluentSQLSerializerMSSQL.AsStringremontava o corpo da consulta por conta própria em vez de delegar aComposeSqlCore, e por isso o ramo doUNIONe a CTE simplesmente não apareciam no texto emitido — SQL válido e incompleto, sem erro nenhum. Passou a delegar; as duas voltaram. - MSSQL —
AsStringdeixou de escrever no AST. A colunaROW_NUMBER() OVER(...) AS ROWNUMBERera injetada emAAST.Select.Columnsdurante a serialização, então duas chamadas deAsStringna mesmaIFluentSQLacumulavam duas colunasROWNUMBER. Com a migração paraOFFSET/FETCHa injeção deixou de existir eAsStringficou idempotente. (A não-idempotência deAsStringpor outras causas não foi investigada nesta entrega.) Select.DistinctlevantavaExceptioncrua em MSSQL, MySQL, PostgreSQL e Oracle, mesmo SEM paginação. Os quatro tratavamsqDistinctcomo qualificador desconhecido dentro do laço de paginação, que os serializadores chamam incondicionalmente. O laço, que estava duplicado nos nove drivers, virouTFluentSQLSelectQualifiers._Pagination.Skip(n)semFirst(m)emitia SQL inválido em MSSQL, Oracle, MySQL e SQLite. Ver a tabela em Changed.- Oracle — o embrulho
SELECT * FROM (SELECT T.*, ROWNUM AS ROWINI ...)acrescentava a colunaROWINIao resultado. Uma consultaSelect.All.From('T').First(n)devolvia uma coluna que o usuário nunca pediu. Medido: cinco valores por linha numa tabela de quatro colunas. Sem embrulho, some. MERGEcom lista de pares de contagem ímpar emitia SQL inválido em silêncio. Ver a entrada BREAKING em Changed — a correção troca "não levantava nada" por "levantaEArgumentException", e por isso não cabe aqui.- Documentação — o guia
dml-merge.mdensinava duas formas que nunca funcionaram. O exemplo principal usava.Update(['T.VALOR = S.VALOR', 'T.DATA = S.DATA']), tratando o array como lista de fragmentos de SQL; o array sempre foi lido como pares nome/valor, e essa chamada emiteUPDATE SET T.VALOR = S.VALOR = ..., que o SQL Server recusa comMsg 102, Incorrect syntax near '='. A mesma página anunciava um overload.Insert(['ID','VALOR'], ['S.ID','S.VALOR'])de dois arrays que não existe emIFluentSQLMergeWhenNotMatched— copiar aquela linha dáE2034 Too many actual parameters. A página foi reescrita com a forma correta, com aviso explícito de que a anterior era inválida, e com a tabela de suporte por dialeto refletindo o que cada um faz hoje. O overload de dois arrays não foi criado: colunas e valores vão no mesmo array, alternados.
Known issues
RESOLVIDO — ver "DELETEcomJOINcontinua emitindo SQL que os motores recusam — a porta irmã, deixada aberta de propósito.JOINdentro de umDELETEdeixou de emitir SQL", em Changed.A previsão que esta dívida registrava estava ERRADA, e fica escrito porque a correção é do argumento e não só do estado. O texto dizia "o irmão é TRADUZÍVEL, não recusável" e "lá a saída provável é emitir a forma válida de cada dialeto, não levantar". A medição nos sete motores desmentiu isso por um caminho que a revisão de então não tinha percorrido: ela mediu só o
InnerJoin. ComLeftJoine semWhere, a forma nativa que os dois motores permissivos aceitam apaga a tabela inteira —A: 4 → 0 — e reporta sucesso, porque na junção externa a condição não filtra nada.InnerJoinfiltra,LeftJoinnão: a família não tem uma semântica, e traduzi-la escolheria pelo usuário entre dois resultados medidos como diferentes. A porta foi recusada.Continua verdadeiro o que a dívida afirmava sobre o
InnerJoinisolado: existe forma portável e ela é aceita pelos sete. Isso virou tarefa própria, e a interseção não é vazia.Achado pré-existente que ia junto — atendido de lado, e é bom que se diga como:
Query(dbnMongoDB).Delete.From('A').InnerJoin('B')…levantavaEArgumentOutOfRangeException: List index out of bounds (0), erro cru deTListsem nome de método. A guarda nova é de núcleo e alcança odbnMongoDBpelo mesmo caminho dos demais, então a recusa que ele passa a receber é nomeada. Isso é efeito, não promessa: o MongoDB está fora da interseção relacional e fora de toda contagem desta entrega.Firebird —
Union+ paginação pagina apenas o primeiro ramo. O FluentSQL emiteSELECT FIRST 3 SKIP 20 * FROM T UNION SELECT * FROM U; no Firebird oFIRST/SKIPescrito num ramo recorta aquele ramo, não o resultado doUNION— medido, devolve 63 linhas onde os outros seis dialetos devolvem 3. É SQL válido com semântica divergente. Não corrigido: o conserto exige embrulhar oUNIONnuma subconsulta, mudando substancialmente a forma emitida por este driver. Detalhes emtest.pagination.firebird.sql, parte 3.MSSQL —
WITH(CTE) eUNIONcombinados comOrderBydo usuário geram SQL inválido, com ou sem paginação.FluentSQL.Serialize.pasmonta oORDER BYdentro deLBasee só depois embrulha na CTE ou concatena oUNION, produzindoWITH CTE AS (SELECT ... ORDER BY ...)(Msg 1033) eSELECT ... ORDER BY ... UNION SELECT ...(Msg 156). É defeito de composição, independente de paginação, e não foi corrigido nesta entrega. Casos I e J detest.pagination.mssql.sql.First(0)acrescenta um segundo motivo de invalidez ao mesmo caso. Com operação de conjunto eOrderBydo usuário, o driver emite a caudaOFFSET 9223372036854775807 ROWSsem umORDER BYna frente — porque oORDER BYdo usuário já foi consumido dentro do primeiro ramo pelo defeito acima — e o resultado éMsg 156. Não é regressão: a mesma combinação já era inválida antes desta entrega (pelo motivo do parágrafo anterior), e na base79450e9oUNIONera descartado em silêncio. Vale registrar porquecPULA_TUDO(FluentSQL.SerializeMSSQL.pas) é o único ponto do driver que pode sair semORDER BYna frente: em todos os outros caminhos a cláusula é garantida porPaginationOrderBy. Quando o defeito de composição for corrigido, este segundo motivo desaparece junto — mas quem for mexer ali precisa saber que existem os dois.Fora de escopo, confirmado pré-existente:
First(0)comFrom(<subconsulta>)sai comoMsg 102(derived table sem alias). Independe deFirst(0)e já ocorria antes desta entrega.MongoDB —
UnionlevantaEIntfCastError, com ou sem paginação. Não é defeito de paginação; o MongoDB não temUNIONe deveria recusar com exceção nomeada, como já faz para CTE (EFluentSQLMongoDBSerialize).MongoDB —
MERGEé descartado em silêncio.FluentSQL.SerializeMongoDB.passobrescreveAsStringpor inteiro e nunca chega à seção deMERGE, então a cláusula some e a saída é{}. Não vaza valor do usuário, mas também não avisa — é o único dos nove que não levanta nem emite. Como a parametrização acontece na construção, antes da serialização, o valor ainda entra emParams:Params.Count = 1referenciado por nada no MQL. Não corrigido nesta entrega: exige decidir se o MongoDB recusaMERGEcom exceção nomeada ou o mapeia para$mergedo aggregation pipeline, e as duas são decisões de escopo próprio. Travado porTestMerge_MongoDB_DropsMergeSilently_KnownGap, que afirma tanto o{}quanto o parâmetro órfão — fechar a lacuna quebra o teste de forma visível.O nome da coluna em
MERGEcontinua injetável — vale para toda a biblioteca, não só para oMERGE. A correção desta entrega fechou o slot de valor; o slot de identificador não pode virar parâmetro e osQuotedName/Quotedos 9 dialetos envolvem o nome no delimitador sem duplicar o delimitador interno. Medido em SQL Server 2022: nome de colunaNOME] = 'x'; DROP TABLE USERS; --emiteSET [NOME] = 'x'; DROP TABLE USERS; --] = @p1;e a tabela foi dropada. Não é regressão — comportamento idêntico antes e depois. Não corrigido porque o escape de delimitador de identificador toca todos os dialetos e colide com o passthrough porStartsWith/Containsque hoje permite passar nome já qualificado ([dbo].[T]); é decisão de arquitetura própria. Não construa nome de objeto a partir de entrada não confiável. Detalhes e medição na seção FRONTEIRA detest.merge.mssql.sql.array of constem posição de expressão continua interpolando string verbatim. Vale para todoarray of constque não seja um dos quatro slots de valor — hoje 17 pontos de entrada públicos, e não os 6 que uma versão anterior desta entrada listava. São elesWhere,AndOpe,OrOpe,Column,Having,OnCond,CaseExpr,ForDialectOnlyeExpressionemIFluentSQL;AndOpe,OrOpe,OpeeFunemIFluentSQLCriteriaExpression;When,AndOpeeOrOpeemIFluentSQLCriteriaCase; eOnemIFluentSQLMerge. A tabela comarquivo:linhade cada um está na entrada BREAKING doMERGE, em Changed.Em todos eles escalares numéricos viram
:pNe strings seguem literais, por serem tratadas como fragmento de SQL (identificador, operador, trecho)..On(['t.NOME =', 'x''; DROP...'])emiteON (t.NOME = x'; DROP...), e.AndOpe(['NOME =', <entrada>])faz o mesmo depois de umWhere. Isto não mudou nesta entrega e é intencional — é o que permite compor expressão. Mudaram apenas os quatro pontos em que o array é comprovadamente uma lista de valores e o slot não tem como ser fragmento:Merge.Update,Merge.Insert,SetValue(nome, [...])eValues(nome, [...]).O critério mecânico, para quem for auditar: passa por
TUtils.SqlArrayOfConstToParameterizedSql→ literal; passa porTUtils.SqlArrayOfConstToParameterizedValue→ parâmetro.Não há terceiro caminho.Passou a haver: o slot de valor deIfThen/ElseIf(entrada do slot de valor doCASE, em Added) chamaIFluentSQLParams.Adddireto, porque o valor não vem de umarray of const. O critério acima continua correto paraarray of const, que é do que esta entrada trata; para a API inteira, a recontagem está na entrada doCASE. Um caminho seguro para expressão continua sob tarefa própria.Consequência que vale declarar: como o
INSERT/UPDATEnão exprime expressão em slot de valor por nenhum overload (o tipadoSetValue(String, String)também parametriza), não há hoje forma suportada de escreverSET DATA = CURRENT_TIMESTAMPpelo construtor de valores. A distinção entre valor e expressão que separa os dois casos está sob revisão como tarefa própria, e é ela que decidirá se esse caso ganha caminho próprio.O— RESOLVIDO NESTE MESMO BLOCO. Este item é REGISTRO HISTÓRICO, não estado atual.CASEnão tem slot de valor⚠️ Leia isto antes do resto do item. O slot de valor existe:
IfThen(AValue: Variant; ADataType: TFluentSQLDataFieldType)eElseIf(...)igual, entregues em Added neste mesmo[Unreleased]. Se você precisa ligar um valor numTHEN/ELSE, use essa sobrecarga — não a deString, que continua sendo posição de expressão e interpola verbatim. O texto abaixo descreve a situação até a entrega do slot de valor, e é mantido porque a medição que ele registra é o que justifica a forma escolhida (CAST(:pN AS <tipo do dialeto>)).Até a entrega do slot de valor,
IFluentSQLCriteriaCase.IfThen/ElseIftinham duas sobrecargas —StringeInt64— e as duas punham o argumento verbatim no texto do SQL. A deStringé posição de expressão, pela mesma regra dos 17 acima (é o que permiteIfThen('SALARIO * 1.1'), e é como a própria suíte usa:IfThen('''FISICA'''), com o chamador pré-aspando) — e continua sendo, inalterada. A deInt64passa porIntToStre não pode injetar nada, mas também não vira:pN— e também continua assim. O que faltava, e passou a existir, era uma terceira sobrecarga que ligasse o valor.A correção natural — fazer
THEN/ELSEemitirem:pNnu — foi medida antes de ser escrita, e dois dos sete motores recusam essa forma noPREPARE. É por isso que o slot entregue não emite:pNnu, e simCAST(:pN AS <tipo do dialeto>):Motor CASE WHEN c THEN :p1 ELSE :p2 ENDna projeçãoo mesmo dentro do WHEREPostgreSQL 16.14 aceita aceita MySQL 8.4.11 aceita aceita SQLite 3.53.4 aceita aceita SQL Server 2022 (16.0.4265.3) aceita aceita Oracle AI Database 26ai 23.26.2.0.0 aceita aceita Firebird 5.0.4 -804 Data type unknown(SQLSTATE HY004)aceita DB2 v12.1.5.0 SQL0418N ... untyped parameter marker(SQLSTATE 42610)recusa também InterBase não medido — sem imagem de container pública não medido Isolado: no Firebird o
-804não é doCASE—SELECT :a FROM RDB$DATABASEdá o mesmo erro. É o marcador sem tipo em posição onde nada lhe dá tipo. ComCASTnos dois ramos, os dois motores aceitam. Fazer o slot funcionar nos sete exigia emitirCAST(:pN AS <tipo do dialeto>)— despacho por driver, desenho novo, que na época colidia com a tarefa então em aberto sobreCastsem despacho. Versões,docker run, forma exata doPREPAREem cada cliente e saída bruta de cada motor emTest Delphi\Common_tests\test.cases.bind.matrix.sql.Como a lacuna foi fechada, em duas etapas, as duas neste mesmo
[Unreleased]:- O
Castganhou despacho por dialeto (entrada própria em Added): saiu do padrão A e cada dialeto passou a devolver a própria grafia. Isso removeu o "desenho novo" que bloqueava — e, medido com as strings exatas dos drivers, os dois motores que recusavam passaram a passar doPREPARE. - O slot de valor foi criado (entrada própria em Added, com
BREAKING CHANGE (API)em Changed), emitindoCAST(:pN AS <tipo do dialeto>)nos sete — a alternativa (i) da lista que este item deixava para o dono. A decisão foi tomada; não há mais escolha pendente aqui. As outras duas alternativas — parametrizar só onde o motor aceita, ou não criar o slot — foram descartadas, e a razão está na entrada de Added.
O que continua verdadeiro deste item: as sobrecargas de
StringeInt64seguem em posição de expressão, verbatim, inalteradas — de propósito, porque é o que permite compor expressão num ramo. O que mudou é que existe alternativa para quem tem um valor, e não havia.Atenção a quem citar a versão do Oracle: a tag
gvenzl/oracle-free:23-slimentrega Oracle AI Database 26ai 23.26.2.0.0, não 23c.Lacuna que permanece, e é de outra família:
_AssertValueSlotFreeprotege a substituição do ramo, não a composição sobre ele..When('1').IfThen('A', dftString).AndOpe('X = 1')emiteTHEN (CAST(:p1 AS VARCHAR(4000))) AND (X = 1)— termo estranho, mas não vaza parâmetro, e o comportamento é idêntico com a sobrecarga deString. É pré-existente, não é regressão do slot de valor, e está catalogado na dívida do builder (AndOpe/OrOpeem posição inesperada).- O
FluentSQL.Select.pas—TFluentSQLSelect.Serializeé código morto. Os 9 drivers sobrescrevemSerializeeFluentSQL.Ast.passempre pega a instância doRegister; reverter a linha corrigida não derruba teste algum. A forma neutra foi corrigida ali por coerência, não por efeito observável.A matriz de paginação vive apenas no projeto Firebird.
PTestFluentSQLFirebird.dpré o único que compilatest.pagination.filter.pas. Consequência medida: reverter a caudaLIMIT/OFFSETdo SQLite derruba 4 testes, todos ali —SQLite_testsnão tem teste de paginação nenhum. A cobertura existe, mas mora longe do driver que protege.MongoDB —
Abs,Cast,Upper,Lower,RoundeFloorgeram MQL inválido em silêncio. Essas seis são do "padrão A" (o núcleo emite SQL ANSI sem consultar o driver), eFluentSQL.SerializeMongoDB.passó reconhece nome de campo e os prefixos de agregação (AGG:,SUM(,COUNT(,MIN(,MAX(,AVG(,AVERAGE(). O resultado é que a coluna é descartada sem exceção:.Column(Fun.Round('v',2))produz{"find":"t","filter":{},"projection":{}}— a projeção sai vazia — enquanto.Column(Fun.Sum('v'))produz corretamente{"aggregate":"t","pipeline":[{"$project":{"x":1,"_id":0}}],...}. É o mesmo modo de falha que foi corrigido paraCeileLength, e sobrevive nestas seis. Não corrigido nesta entrega — o conserto exige mover as seis para o "padrão B" (implementação por driver nos 9 dialetos) ou ensinar o serializador MongoDB a montar$projectcom expressão, e nenhum dos dois cabia no escopo. Registrado como dívida; até lá, não use funções escalares na projeção comdbnMongoDB.
[1.5.1] — 2026-04-20
Fixed
- Pipeline — Documentation Build (Issue #144): Resolved
tsconfig.jsonconfiguration conflict indocs-srcrelated tobaseUrland path mapping, ensuring compatibility with Docusaurus 3. - Pipeline — Governance Sync (Issue #144): Restored pipeline synchronization by resolving drift for ESP-077 (Issue #141) and finalizing governance state.
[1.5.0] — 2026-04-20
Added
- DDL — Schema Support (ESP-075, issue [#142]):
CREATE SCHEMAandDROP SCHEMAsupport for PostgreSQL, MSSQL, and MySQL (mapped to DATABASE per ADR-075). - DML — MERGE Skeleton (ESP-076, issue [#142]): Fluent API for
MERGE INTO ... USING ... ON ... WHEN MATCHED/NOT MATCHED. Initial serialization for MSSQL. - DDL — Advanced Truncate Support (ESP-074, issue [#136]):
- Support for multi-table truncation in a single atomic command across all supported dialects.
- PostgreSQL: added
RESTART IDENTITY,CONTINUE IDENTITY, andCASCADEsupport. - MySQL: added
PARTITIONsupport for single-table truncation. - Consistent validation and
ENotSupportedExceptionfor advanced options in dialects that do not support them.
[1.4.0] — 2026-04-17
Added
- MongoDB — Aggregations & Joins (ESP-067, ESP-068, issues [#86], [#87]): comprehensive SQL-to-MQL mapping for
GROUP BY,HAVING,INNER JOIN, andLEFT JOIN(using$lookup,$group,$unwind, and$projectstages). - MongoDB — DDL Extensions (ESP-065, ESP-066, issues [#83], [#84], [#85]): added support for Capped Collections, TTL Indexes, Index Management (Create/Drop), Collection Rename and Truncate.
- DDL — Procedural Support (ESP-070, ESP-071, issues [#134], [#135]): added comprehensive support for Stored Procedures, Triggers, and Stored Functions across PostgreSQL, Firebird, MS SQL Server, and MySQL. Includes support for
OR REPLACE,IF EXISTS, and trigger management (Enable/Disable). - DDL — Core Expansion (issues [#67], [#68], [#69], [#70], [#71], [#72], [#73], [#74], [#75], [#79], [#82]):
- Native Identity / Auto-Increment support and Advanced Identity (
ALWAYS/BY DEFAULT) for PG, FB, and Oracle. - Alter Column and Computed Columns support.
- Native CREATE/DROP VIEW and SEQUENCE support.
- Table and Column Comments support.
- Composite/Named Constraints and Alter Table constraint management.
- Dialect-specific support for MongoDB DDL.
- Native Identity / Auto-Increment support and Advanced Identity (
Changed
- DDL — API Refactoring (issue [#80]): transitioned DDL API to a "context-first" pattern to improve readability and consistency across dialects.
- Repository Strategy: finalized test suite fragmentation to better handle dialect-specific integration tests.
- Documentation: updated DDL guides and README to reflect the expanded feature set and new entry points.
[1.3.0] — 2026-04-14
Added
- DDL — Rename Table Support (ESP-047, issue [#65]): added
.AlterTableRename(const AOldName, ANewName: string)toSchemaAPI, with dialect-specific serialization for Firebird, PostgreSQL, MySQL, MSSQL (usingsp_rename), and SQLite. - DDL — GUID Type Support (ESP-043, issue [#63]): centralized GUID literal translation and added support for GUID types in DDL column definitions.
- DDL — Logic Centralization (ESP-042, issue [#60]): mandatory identifier quoting and refactored DDL serialization logic into a robust abstract layer.
- Core — Portability (ESP-046, issue [#64]): removed
Winapi.Windowsdependency fromFluentSQL.DDL.pasfor better cross-platform support.
Fixed
- MSSQL — Boolean Serialization (issue [#62]): fixed compatibility issues with boolean literal serialization in MSSQL environments.
- Pipeline — Root Cleanliness (issue [#64]): enforced "no root pollution" policy, redirecting temporary artifacts to
.local-readonly/.
Tests
- DUnitX:
TTestDDLAlterTableRenameTablecoverage for all 5 core dialects. - Verified 157 passing tests in the Firebird suite.
[1.2.0] — 2026-04-13
Added
- DDL — Foreign Keys (ESP-035, issue [#49]): added
.References(const ATableName, AColumnName: string)support forCREATE TABLEandALTER TABLE ADD COLUMN(Firebird, PostgreSQL). - DDL — Advanced Constraints (ESP-034, issue [#48]): support for
NOT NULL,DEFAULT, andPRIMARY KEYinCREATE TABLEandALTER TABLE ADD COLUMN. - Cache — Redis Provider (ESP-032, issue [#47]): distributed SQL string caching with support for Redis as a back-end, including deterministic AST-based hashing (
TFluentSQL.AsString) to prevent cache collisions. - DDL — RENAME COLUMN/TABLE (ESP-030, ESP-031, issues [#45], [#46]): added
.RenameToandRenameColumnfor PostgreSQL, Firebird, and MySQL. - DDL — TRUNCATE TABLE (ESP-029, issue [#44]): fluent API for
TRUNCATE TABLEand serialization across multiple dialects. - DDL — DROP INDEX improvements (ESP-025, ESP-026, ESP-027, ESP-028, issues [#40], [#41], [#42], [#43]): added
DROP INDEX, support forIF EXISTS,CONCURRENTLY(PostgreSQL), andON table(MySQL/MariaDB).
Changed
- Documentation: updated DDL guides (foreign keys, renaming, create table) and API reference. Root directory cleanup (
VISIBILIDADE-EXECUCAO.mdremoved).
[1.1.1] — 2026-04-10
Added
- DDL — ALTER TABLE DROP COLUMN (ESP-020, ADR-020, issue #34):
CreateFluentDDLAlterTableDropColumn,DDLAlterTableDropColumnSQLe serialização Firebird/PostgreSQL; guiaddl-alter-table-drop-column.md. - DDL — CREATE [UNIQUE] INDEX (ESP-022, ADR-022, issue #35):
CreateFluentDDLCreateIndex,DDLCreateIndexSQL(Firebird, PostgreSQL); guiaddl-create-index.md; testes DUnitX adicionais (incl. multi-coluna Firebird e nomes vazios).
Changed
- Documentação: alinhamento do índice do portal e da referência de API com ESP-020/ESP-022; quadro de visibilidade de execução em
VISIBILIDADE-EXECUCAO.md(ESP-024).
Tests
- DUnitX:
TTestDDLAlterTableDropColumn,TTestDDLCreateIndexemTest Delphi/test.ddl.pas(suíte Firebird).
[1.1.0] — 2026-04-09
Added
- Extensão explícita por motor (ESP-016, ADR-016, issue #27): API de opt-in por dialeto (
ForDialectOnly/ serialização por motor), alinhada aFluentSQL.Serialize.pase documentação emdocs-src. - DDL — CREATE TABLE (ESP-017, ADR-017, issue #28): API fluente e
DDLCreateTableSQLpara Firebird e PostgreSQL comTDDLLogicalType. - DDL — DROP TABLE (ESP-018, ADR-018, issues #29 / #30): API fluente e serialização de texto SQL para
DROP TABLE. - DDL — ALTER TABLE ADD COLUMN (ESP-019, ADR-019, issue #31): uma coluna lógica por
AsString, reutilização do mapeamento de tipos do CREATE e paridade Firebird/PostgreSQL. - Documentação e CI: portal Docusaurus em
docs-src/,ROADMAP.mdoperacional, fluxos.github/workflows/docs-build.ymledeploy-docs.yml, e guias DDL (ddl-create-table,ddl-drop-table,ddl-alter-table-add-column).
Tests
- DUnitX em
Test Delphi/test.ddl.paspara CREATE/DROP/ALTER (matriz referida nos relatórios de review/test; execução MSBuild sujeita a caveats).
Dívida técnica pós-caveats: #32.
[1.0.9] — 2026-04-08
Added
- INSERT em lote (ESP-015, ADR-014, issue #31):
AddRowno fluente (contextoInsert) fecha a linha corrente e abre a próxima;AsStringfaz flush implícito da linha pendente. SQL:VALUES (...), (...)com placeholders na ordem linha a linha (ADR-009). MongoDB (dbnMongoDB):insertManycomdocumentsquando há mais de uma linha; uma linha continua cominsertOne.
Tests
- DUnitX:
test.core.params(Firebird + MySQL, batch parametrizado);UTestFluentSQLFirebird(insertManyMongo).
Dívida técnica pós-caveats: #32.
[1.0.8] — 2026-04-08
Changed (breaking)
- MongoDB (
dbnMongoDB, ESP-014, issue #29):TFluentSQLSelectMongoDB.Serializedeixa de emitir pseudo-SQLcolecao.find({...})e passa a devolver o fragmento JSON ADR-013 §2b{"collection":"…","projection":{…}}, partilhando a mesma lógica de projeção queTFluentSQLSerializerMongoDB(2b + 2c). Quem dependia da string antiga deve migrar.
Added
- MongoDB — DML (ADR-013 Opção A):
IFluentSQL.AsStringparaInsert/Update/DeleteemdbnMongoDBemite JSON mínimo e estável:insertOne,updateMany(comfilter+update.$set) edeleteMany, com resolução de placeholders:pNnos documentos viaIFluentSQLParams(sem literais:pNno JSON final). TFluentSQL.MongoSelectFragment: fragmento da secção SELECT em JSON paradbnMongoDB(vazio noutros dialetos); útil para testes e introspecção.- WHERE Mongo: prefixo
NOTa nível de expressão →{"$nor":[…]}(um eixo de extensão do parser documentado na ESP-014). - Guardas:
WITH/UNION/INTERSECTemdbnMongoDBlevantamEFluentSQLMongoDBSerializecom mensagem estável em vez de SQL inválido.
Tests
- DUnitX em
UTestFluentSQLFirebird.pas: DML Mongo (insertOne/updateMany/deleteMany), coerênciaMongoSelectFragmentvsAsString, eNOT→$nor.
Dívida técnica pós-caveats: #30.
[1.0.7] — 2026-04-08
Changed
- Parametrização (ESP-013):
TFluentSQL.CaseExpr(array of const)passa a usarSqlArrayOfConstToParameterizedSqlcomFAST.Params(ADR-009, ADR-011, ADR-012). Escalares na expressão discriminante doCASEpassam a placeholders; strings no array continuam fragmentos literais. Rastreio da entrega: esta secção [1.0.7] (não confundir com a issue #27, hoje ESP-016 / fecho formal). Dívida técnica pós-caveats: #28.
[1.0.6] — 2026-04-08
Changed
- Parametrização (ESP-012, issue #25):
TFluentSQL.Column(array of const)passa a usarSqlArrayOfConstToParameterizedSqlcomFAST.Params, alinhado a ADR-009 / ADR-011; escalares na projeção viram placeholders em vez de literais concatenados.CaseExpr(array of const)foi migrado em ESP-013 (ver [1.0.7] acima). Dívida técnica pós-caveats: #26.
[1.0.5] — 2026-04-08
Changed
- Parametrização (ESP-011, issue #23):
TFluentSQLCriteriaExpressionusaSqlArrayOfConstToParameterizedSqlquando associada aIFluentSQLParams(contextoTFluentSQL/TFluentSQLCriteriaCase);Expression(array of const)eExpression(string)no fluente recebem a coleção do AST. Sobrecargasarray of constem joins/having/where herdam o mesmo contrato da ESP-010; strings emTVarReccontinuam sem bind automático (ADR-011).CaseExpr(array of const)eColumn(array of const)permanecem comSqlParamsToStr.
Added
- Testes DUnitX em
test.core.params.pascobrindo critérios/expressão comarray of constviaWhere. Dívida técnica pós-caveats: #24.
[1.0.4] — 2026-04-08
Changed
- Parametrização (ESP-010, issue #21): sobrecargas com
array of constemWhere,AndOpe,OrOpe,Having,OnCond,SetValue,Values(FluentSQL.pas) eWhen,AndOpe,OrOpeemTFluentSQLCriteriaCase(FluentSQL.Cases.pas) passam a expandir valores escalares (inteiro, int64, extended, currency, boolean, variant numérico/data) viaIFluentSQLParamse placeholders (:pNno AST); entradas textuais emTVarRec(identificadores, operadoresChar, strings) continuam literais na expressão, alinhado a RN-P3. Helper novo:TUtils.SqlArrayOfConstToParameterizedSql.CaseExpr(array of const),Column(array of const)eExpression(array of const)mantêmSqlParamsToStr(expressão mista / nomes).
Added
- Testes DUnitX em
test.core.params.pas:Where/Having/Values/CASE WHENcomarray of conste parâmetros. Dívida técnica pós-caveats: #22.
Fixed
Test Delphi/MSSQL_tests/test.select.mssql.pas: asserts deWHEREcomGreaterEqThan/LessEqThanesperam:p1/:p2;ORDER BYespera sufixoASC, coerente com a serialização atual (runner usaCreateFluentSQL(dbnFirebird)nestes casos).
[1.0.3] — 2026-04-08
Changed
- Parametrização (ESP-009): predicados
IN/NOT INcom listas (TArray<String>,TArray<Double>) passam a emitir placeholders (:p1,:p2, …) e a preencherIFluentSQLParamspor elemento; subconsultas emInValues(string)/NotIn(string)continuam literais entre parênteses. Operadores SQL normalizados paraINeNOT INna concatenação. Ver issue #19. - Testes Firebird
test.operators.isin.firebird(ligado aPTestFluentSQLFirebird.dpr) e suíte MySQL (test.functions.mysql,test.select.mysql) alinhados a placeholders eORDER BY … ASConde a serialização atual já produz esse formato.
Added
- Fixture DUnitX
Test Delphi/test.core.params.pasintegrado emPTestFluentSQLFirebird.dpreTestFluentSQL_MySQL.dpr(cenários Firebird, MySQL e PostgreSQL no runner Firebird). Dívida técnica pós-caveats: #20.
[1.0.2] — 2026-04-08
Changed
ROADMAP.md: encerramento da Fase 0 no âmbito consumidor após auditoria ESP-008; checklist R1–R6 com evidências citáveis; bloco Estado atual com próximo foco na Fase 1 (parametrização / prepared statements). Ver issue #17. Dívida técnica pós-caveats: #18.
[1.0.1] — 2026-04-08
Added
ROADMAP.mdcomo artefato operacional e evolutivo: política de gatilhos (/architect,/sprint,/release, conclusão de implementação), histórico de evolução, estado/foco ligado ao pipeline (.claude/pipeline/), fase Meta — Governança do roadmap e alinhamento checklist ↔ relatórios. Ver issue #15. Dívida técnica pós-caveats: #16.
[1.0.0] — 2026-04-08
Changed (breaking)
- API: a fábrica global
CQueryfoi substituída porCreateFluentSQLna unitFluentSQL.pas. Código que chamavaCQuery(dbn…)deve usarCreateFluentSQL(dbn…). (O nomeNewFluentSQLdo ADR foi evitado: em Delphi o tokenNewpode ser interpretado como o intrínsecoNew, quebrando encadeamentos como.&As(…)após a chamada da fábrica.) - Testes / Boss / metadados: projetos DUnitX renomeados para o prefixo
TestFluentSQL_*; pacote Boss passa a publicar-se como FluentSQL (antesCQuery4D). Unidades e fixtures de teste deixam de usarCQL/TCQL.Newem favor deFluentSQL/CreateFluentSQL.
| Antes | Depois |
|---|---|
CQuery(dbnFirebird) |
CreateFluentSQL(dbnFirebird) |
TCQL.New(dbnMSSQL) |
CreateFluentSQL(dbnMSSQL) |
TCQL.SetDatabaseDafault(...) |
TFluentSQL.SetDatabaseDafault(...) |
uses CQL, CQL.Interfaces |
uses FluentSQL, FluentSQL.Interfaces |
CQL.Q('x') (literais em CASE) |
TFluentSQLFunctions.QFunc('x') com FluentSQL.Functions em uses |
Ver issue #13. Dívida técnica pós-caveats: #14.
[0.2.0] — 2026-04-08
Added
- Planejamento da evolução do framework para suporte a recursos avançados de SQL (CTE, Window Functions, etc).
- Planejamento de melhorias de segurança através de Prepared Statements (Parametrização).
- Planejamento de novos serializadores (MongoDB MQL, REST API).
- Mescla ordenada de parâmetros em operações de conjunto (
UNION,UNION ALL,INTERSECT): coleçãoParamsalinhada à ordem dos placeholders na SQL final, com reindexação do ramo secundário e suporte MySQL (?). Ver issue #11. - Módulo
FluentSQL.Params.pascom visão mesclada de parâmetros em queries compostas. - Testes DUnitX (Firebird e MySQL) cobrindo parâmetros nos dois lados do conjunto.
Changed
- Serialização de conjuntos e driver MySQL para contagem total de placeholders em
UNION. - Ajustes correlatos em AST, operadores, interfaces e registro de drivers (MongoDB, Firebird, SQLite) integrados à entrega revisada.
[0.1.0] — 2026-04-07
Added
- Versão inicial do projeto documentada no Ecossistema Delphi.
- Suporte básico para SELECT, INSERT, UPDATE, DELETE em múltiplos dialetos.
- Abstração via AST (Abstract Syntax Tree).