Skip to content

fix(external-db): elimina traps de vazio silencioso A1 (_search sem coluna) e A2 (array vazio) no REST nativo#535

Merged
adm01-debug merged 1 commit into
mainfrom
fix/rest-native-silent-empty-a1-a2
May 30, 2026
Merged

fix(external-db): elimina traps de vazio silencioso A1 (_search sem coluna) e A2 (array vazio) no REST nativo#535
adm01-debug merged 1 commit into
mainfrom
fix/rest-native-silent-empty-a1-a2

Conversation

@adm01-debug
Copy link
Copy Markdown
Owner

@adm01-debug adm01-debug commented May 30, 2026

Contexto

Com a bridge desligada (edge_external_db_bridge.enabled=false, rollout=100 → 100% REST nativo), qualquer SELECT que escape do caminho REST vira vazio silencioso (a bridge não está mais lá para o fallback). Este PR fecha os dois traps de vazio silencioso identificados na análise do invokeExternalDbrest-native.ts.

Ambos são latentes hoje (nenhum caller vivo os dispara — ver "Blast radius"), então isto é hardening defensivo de baixo risco; o foco foi não regredir o único caminho vivo (_search em products).

A1 — _search em tabela sem coluna de busca tornava a tabela inelegível

isRestNativeEligible retornava false quando _search estava presente mas a tabela não tinha coluna em SEARCH_COLUMNS → (bridge OFF) vazio silencioso. O fix "ingênuo" (só tornar elegível) continuaria quebrado: executeRestNativeSelect aplicava ilike(SEARCH_COLUMNS[t] ?? 'name', …), e das 7 tabelas whitelisted fora de SEARCH_COLUMNS, só product_variants tem coluna name — as outras 6 (product_images, product_kit_components, product_materials, product_videos, print_area_techniques/view, tabela_preco_gravacao_oficial_faixa) dariam 400 → vazio silencioso de novo.

Solução endurecida:

  • _search vira best-effort: elegibilidade não depende mais dele.
  • Novo resolveSearchColumn() (predicado único, sem fallback 'name'): aplica ilike só quando há coluna real; senão serve a query base e emite logger.warn (diagnóstico).

A2 — filtro array vazio → 400 → vazio silencioso

applyFilters traduzia coluna: [] para in(['__no_match__']), que dá 400 (invalid input syntax) em colunas uuid/numéricas → retry (+500ms) → vazio silencioso. Agora executeRestNativeSelect curto-circuita para {records:[],count:0} sem ir à rede — que é a semântica correta de IN () (zero linhas). Não chama reportSilentEmpty (é vazio correto, não trap).

Robustez adicional (achados da simulação)

  • F2: _search vazio/whitespace/não-string passa a ser tratado como "sem busca" (normalizeSearchTerm) — antes derrubava a query / gerava ilike('% %').
  • F3: _search é extraído antes do scan de array vazio, então _search: [] não é confundido com filtro IN () vazio.
  • F4: A2 tem precedência sobre _search (short-circuit retorna antes de montar a query).

Escopo e follow-up

  • Apenas SELECT. O caminho de ESCRITA (executeRestNativeWrite via applyFilters) ainda tem o sentinel ['__no_match__']: um update/delete com filters:{id:[]} passa o guard anti-mutação-em-massa e dá 400 → throw LOUD (toast). É barulhento, não silencioso → prioridade menor.
  • F5 (follow-up): decidir se um update/delete com escopo array-vazio deve ser no-op limpo (0 linhas) ou erro explícito. Deixado fora deste PR de propósito (mudar isso altera semântica de escrita e poderia mascarar bug de caller). Abrir como issue/PR separado após o merge.

Mudanças

  • src/lib/external-db/rest-native.ts — 4 alterações cirúrgicas (helpers resolveSearchColumn/normalizeSearchTerm; elegibilidade best-effort; short-circuit A2; ilike condicional). Nenhuma outra linha tocada.
  • src/lib/external-db/rest-native.test.ts — vitest cobrindo A1, A2, F2, F3, F4, regressão de products e elegibilidade.

Blast radius (verificado)

  • Todos os callers vivos de _search batem em table: 'products' (kit-builder, lightweight, simulator) → caminho protegido por teste de regressão.
  • Callers de filtro-array (category_id/supplier_id em useProductsLightweight) são guardados por .length > 0 → não chegam vazios.

Validação

Rodada localmente (ambiente isolado, stubs fiéis às assinaturas) — detalhes em comentário:

  • tsc (typecheck strict) — 0 erros (código + teste)
  • vitest runrest-native.test.ts 13/13 passando
  • eslint (config exata do repo, parser type-aware, --max-warnings=0) — 0 erros / 0 warnings
  • lint:baseline (check-eslint-baseline.mjs) — CI do repo (sem regressão esperada: arquivos saem com 0 warnings)
  • build Vite — CI do repo

Após CI verde, remover o status de draft e revisar.

…oluna) e A2 (filtro array vazio) no REST nativo

A1 — _search em tabela whitelisted sem coluna de busca tornava a tabela
INELEGÍVEL → (bridge OFF) vazio silencioso. Agora _search é best-effort:
elegibilidade não depende mais dele; o ilike só é aplicado quando há coluna
real (sem fallback para 'name', que dava 400 em product_images/etc.).

A2 — filtro array vazio gerava `in(['__no_match__'])` → 400 (uuid/numérico) →
retry + vazio silencioso. Agora curto-circuita para {records:[],count:0} sem
rede (semântica correta de `IN ()`), sem reportSilentEmpty.

Também corrige F2 (_search vazio/whitespace/não-string = sem busca) e blinda F3
(extrai _search antes do scan de array vazio) e F4 (A2 tem precedência).

Escopo: apenas SELECT. Caminho de ESCRITA (F5) fica como follow-up.
Inclui rest-native.test.ts (vitest) cobrindo A1/A2/F2/F3/F4 + regressão de products.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
we-dream-big Error Error May 30, 2026 10:27pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d0e7933a-7e5e-4436-8ed1-0c6195dfede0

📥 Commits

Reviewing files that changed from the base of the PR and between 2c73a75 and ec118af.

📒 Files selected for processing (2)
  • src/lib/external-db/rest-native.test.ts
  • src/lib/external-db/rest-native.ts

Walkthrough

PR adiciona _search best-effort e short-circuit de array vazio em fallback REST-native: helpers de normalização e resolução de coluna, elegibilidade relaxada, extração de _search antes de filtros, e retorno cedo em IN () vazio sem rede. Testes cobrem elegibilidade, comportamento por tabela, e precedência de curto-circuito.

Changes

REST-native: _search best-effort e short-circuit de array vazio

Layer / File(s) Summary
Helpers de busca textual e elegibilidade relaxada
src/lib/external-db/rest-native.ts (linhas 93–120, 304–306)
resolveSearchColumn localiza coluna de busca via aliases, normalizeSearchTerm valida termo, e isRestNativeEligible deixa de desqualificar tabelas por ausência de coluna mapeada.
Execução: short-circuit de array vazio e ilike com coluna resolvida
src/lib/external-db/rest-native.ts (linhas 374–394, 411–422)
_search extraído e normalizado antes de filtros; arrays vazios (IN ()) retornam {records: [], count: 0} cedo sem rede; ilike usa resolveSearchColumn e loga warn se coluna não existir.
Testes: setup de mocks e elegibilidade
src/lib/external-db/rest-native.test.ts (linhas 1–75)
Mocks hoisted, makeQueryStub com registro de chamadas, validação de elegibilidade para whitelisted com _search, não-whitelisted e escrita.
Testes: comportamento de _search por tabela
src/lib/external-db/rest-native.test.ts (linhas 76–115)
products aplica ilike(name, %termo%), product_images sem ilike, _search whitespace/não-string não dispara ilike mas query executa.
Testes: array vazio e precedência com _search
src/lib/external-db/rest-native.test.ts (linhas 117–162)
Short-circuit em qualquer coluna, array vazio precedência, _search:[] removido antes do scan, arrays não-vazios disparam .in().

Sequence Diagram

flowchart TD
    A["entrada: _search, filtros, tabela"] --> B["normalizeSearchTerm(_search)"]
    B --> C["extrair _search dos filtros"]
    C --> D{"existe array vazio<br/>em qualquer filtro?"}
    D -->|sim| E["retorna {records: [], count: 0}<br/>sem ir à rede"]
    D -->|não| F["montar query base<br/>from(tabela)"]
    F --> G{"_search normalizado<br/>existe?"}
    G -->|sim| H["resolveSearchColumn(tabela)"]
    H --> I{"coluna de busca<br/>mapeada?"}
    I -->|sim| J["query.ilike(coluna, %termo%)"]
    I -->|não| K["logger.warn(...)<br/>ignora _search"]
    J --> L["aplicar outros filtros<br/>com .eq(), .in(), etc"]
    K --> L
    L --> M["aplicar orderBy, range,<br/>contar e mapear"]
    G -->|não| L
    M --> N["executar query e retornar"]
    E --> O["fim"]
    N --> O
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/rest-native-silent-empty-a1-a2

Comment @coderabbitai help to get the list of available commands and usage tips.

@supabase
Copy link
Copy Markdown

supabase Bot commented May 30, 2026

This pull request has been ignored for the connected project doufsxqlfjyuvxuezpln because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@adm01-debug adm01-debug marked this pull request as ready for review May 30, 2026 23:23
Copilot AI review requested due to automatic review settings May 30, 2026 23:23
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@adm01-debug adm01-debug merged commit 62bd7c9 into main May 30, 2026
43 of 55 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@adm01-debug adm01-debug deleted the fix/rest-native-silent-empty-a1-a2 branch May 30, 2026 23:23
@adm01-debug
Copy link
Copy Markdown
Owner Author

✅ Validação local — vitest + tsc + ESLint (todos verdes)

Rodei os três gates num ambiente isolado (Node 22) com stubs fiéis às assinaturas dos módulos @/... (supabase, logger, telemetria) e ./bridge (só os tipos). Resultado:

Gate Como rodei Resultado
vitest vitest@3.2.4 run sobre rest-native.test.ts 13/13 passando
tsc tsc --noEmit strict sobre rest-native.ts + teste 0 erros
ESLint config flat exata do repo (ESLint 9.17, @typescript-eslint 8.59, parser type-aware via tsconfig.eslint.json), --max-warnings=0 0 erros / 0 warnings

Cobertura dos testes (13)

  • A1_search em product_images não aplica ilike, serve a query base, emite logger.warn.
  • A2 — filtro array vazio → {records:[],count:0} sem rede (inclui coluna uuid id).
  • F2_search whitespace / não-string = "sem busca".
  • F3_search:[] removido antes do scan, não vira short-circuit.
  • F4 — A2 tem precedência sobre _search.
  • Regressão_search em products continua chamando ilike('name','%abc%').
  • Elegibilidade — whitelist com/sem coluna de busca; não-whitelisted e op de escrita inelegíveis.

Notas de método (honestidade sobre o que NÃO foi coberto localmente)

  • O ESLint usou recommended (não recommended-type-checked) — igual ao repo — então no-unsafe-* / no-floating-promises não estão ativos; os casts as unknown as do mock não geram nada. Prova de fogo: injetei um any numa cópia de produção e o harness acusou (no-explicit-any), confirmando que o lint estava de fato analisando os arquivos.
  • Não reproduzi check-eslint-baseline.mjs nem o build Vite completo — mas como os arquivos saem com zero warnings, não adicionam nada ao .eslint-baseline.json (sem regressão de baseline).

Itens do checklist marcados conforme isso. Os 2 restantes (baseline ESLint + build Vite) ficam pro CI do repo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants