Search by

caiopinheirom / php-qvd

caiopinheirom

Streaming QVD reader for PHP 8.3+

Package info

github.com/caiopinheirom/php-qvd

pkg:composer/caiopinheirom/php-qvd

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 0

v1.0.0 2026-08-27 11:44 UTC

This package is auto-updated.

Last update: 2026-08-27 14:21:53 UTC


README

Streaming QVD reader e query engine para PHP 8.3+.

Biblioteca PHP pura — sem dependência do Laravel. Funciona em PHP puro e em aplicações Laravel 12+ (PHP 8.3+).

Repositório: caiopinheirom/php-qvd

Instalação

composer require caiopinheirom/php-qvd

Enquanto o pacote não estiver no Packagist, use o repositório VCS:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/caiopinheirom/php-qvd"
        }
    ],
    "require": {
        "caiopinheirom/php-qvd": "dev-main"
    }
}

Não há Service Provider nem auto-discovery. A integração é apenas Composer + namespace Qvd\.

Início Rápido

use Qvd\Qvd;

// Abrir arquivo e executar query
$rows = Qvd::from('/caminho/para/dados.qvd')
    ->select('Nome', 'Idade', 'Cidade')
    ->where('Idade', '>=', 18)
    ->orderBy('Nome')
    ->limit(100)
    ->get();

Também é possível usar QvdReader::open() diretamente:

use Qvd\QvdReader;

$reader = QvdReader::open('/caminho/para/dados.qvd');

Metadados

$reader = QvdReader::open('dados.qvd');

// Total de linhas no arquivo (não afetado por filtros)
$total = $reader->rowCount();

// Lista de nomes dos campos
$campos = $reader->fields(); // ['Nome', 'Idade', 'Cidade']

// Schema completo (nome, tipo, tags)
$schema = $reader->schema(); // list<Field>

// Detalhe de um campo específico
$campo = $reader->field('Idade'); // Field { name, type, tags }

Query Builder

Todos os métodos fluent retornam uma nova instância — o reader original nunca é alterado.

select

Restringe quais colunas são retornadas:

$reader->select('Nome', 'Idade')->get();
// [['Nome' => 'Alice', 'Idade' => 30], ...]

where / orWhere

Filtro com operadores =, !=, >, >=, <, <=:

$reader->where('Idade', '>=', 18)->get();
$reader->where('Nome', 'Alice')->get(); // operador = implícito
$reader->where('Idade', '>', 25)->orWhere('Cidade', 'SP')->get();

Grupos aninhados (sub-queries booleanas):

$reader->where(function (QvdReader $q) {
    $q->where('Cidade', 'SP')
      ->orWhere('Cidade', 'RJ');
})->where('Idade', '>=', 18)->get();

whereIn / whereNotIn / orWhereIn

Filtro por lista de valores (otimizado com hash set para listas grandes):

$reader->whereIn('Status', ['ativo', 'pendente'])->get();
$reader->whereNotIn('Tipo', ['teste', 'demo'])->get();

whereNull / whereNotNull

$reader->whereNull('Email')->get();
$reader->whereNotNull('Telefone')->get();

whereBetween / orWhereBetween

Filtro de intervalo inclusivo [min, max]:

$reader->whereBetween('Idade', [18, 65])->get();

Equivalente a where('Idade', '>=', 18)->where('Idade', '<=', 65).

orderBy

Ordena resultados. Nulls sempre ficam por último (tanto asc quanto desc):

$reader->orderBy('Nome', 'asc')->get();
$reader->orderBy('Idade', 'desc')->orderBy('Nome')->get(); // multi-coluna

limit

Restringe a quantidade máxima de linhas retornadas:

$reader->limit(10)->get(); // no máximo 10 linhas

Execução

Os métodos de execução materializam os resultados da query:

$reader = QvdReader::open('dados.qvd')->where('Ativo', true);

// Todas as linhas
$rows = $reader->get();

// Iteração lazy (Generator) — baixo consumo de memória
foreach ($reader->cursor() as $row) {
    // processa linha a linha
}

// Primeira linha (throw se não encontrar)
$first = $reader->first();

// Primeira linha ou null
$maybe = $reader->firstOrNull();

// Existência
if ($reader->exists()) { /* ... */ }

// Contagem
$count = $reader->count();

Processamento em lotes (chunks)

// Generator de batches
foreach ($reader->chunks(1000) as $batch) {
    // $batch é um array com até 1000 linhas
    processBatch($batch);
}

// Callback com controle de parada
$reader->chunk(500, function (array $batch, int $index) {
    saveToDB($batch);
    return $index < 10; // false para interromper
});

Collection Pipeline

Operações funcionais sobre os resultados, avaliadas de forma lazy:

// map: transforma cada linha
$nomes = iterator_to_array(
    $reader->select('Nome')->map(fn($row) => strtoupper($row['Nome']))
);

// filter: filtra com callback
$adultos = iterator_to_array(
    $reader->filter(fn($row) => $row['Idade'] >= 18)
);

// reduce: acumula valor
$soma = $reader->reduce(fn($acc, $row) => $acc + $row['Valor'], 0);

// each: side-effect para cada linha
$reader->each(fn($row) => logger()->info($row['Nome']));

// flatMap: expande cada linha em múltiplos itens
$tags = iterator_to_array(
    $reader->flatMap(fn($row) => explode(',', $row['Tags']))
);

Indexação

Construa um hash index para lookups O(1):

$indexed = $reader->indexBy('CPF');

// Busca por chave — O(1) após construção
$pessoa = $indexed->find('123.456.789-00'); // array|null

Composite keys (chave composta):

$indexed = $reader->indexBy(['Ano', 'Mes']);
$registro = $indexed->find([2024, 6]); // busca por [Ano, Mes]

keyBy / groupBy

// Associativo por campo (last-write-wins para duplicatas)
$byName = $reader->keyBy('Nome');
// ['Alice' => [...], 'Bob' => [...]]

// Agrupado por campo (mantém todas as linhas)
$byCity = $reader->groupBy('Cidade');
// ['SP' => [[...], [...]], 'RJ' => [[...]]]

Joins

Hash join O(n+m) com outro QVD ou array PHP:

$pedidos = QvdReader::open('pedidos.qvd');
$clientes = QvdReader::open('clientes.qvd');

// Inner join por chave comum
foreach ($pedidos->join($clientes, 'ClienteId') as $row) {
    // $row contém campos de ambos os QVDs
}

// Left join (mantém todas as linhas do lado esquerdo)
foreach ($pedidos->leftJoin($clientes, 'ClienteId') as $row) {
    // campos do cliente serão null quando não há correspondência
}

// Join com array PHP
$metadata = [
    ['id' => 1, 'label' => 'Premium'],
    ['id' => 2, 'label' => 'Standard'],
];
foreach ($reader->join($metadata, ['TipoId' => 'id']) as $row) {
    // ...
}

Mapeamento de chaves:

  • String: mesma coluna em ambos os lados → 'ClienteId'
  • Array sequencial: múltiplas chaves com mesmo nome → ['Col1', 'Col2']
  • Array associativo: nomes diferentes → ['left_col' => 'right_col']

Correlação

Enriqueça dados PHP com informações do QVD:

$reader = QvdReader::open('clientes.qvd');

$pedidos = [
    ['pedido_id' => 1, 'cpf' => '111.222.333-44'],
    ['pedido_id' => 2, 'cpf' => '555.666.777-88'],
    ['pedido_id' => 3, 'cpf' => '999.000.111-22'],
];

// Enrich: anexa dados do QVD a cada item
$enriquecido = $reader->enrich($pedidos, 'cpf', 'CPF', 'cliente');
// Cada item terá ['cliente' => [...dados do QVD...]] ou ['cliente' => null]

// Match: filtra itens que existem no QVD
$existentes = $reader->match($pedidos, 'cpf', 'CPF');
// Retorna apenas pedidos cujo CPF existe no QVD

Cache

A biblioteca inclui adaptadores de cache para evitar releitura de headers e symbol tables:

use Qvd\Cache\InMemoryCache;
use Qvd\Cache\Psr16CacheAdapter;

// Cache em memória (padrão interno — lifetime do processo)
$cache = new InMemoryCache();

// Wrapper PSR-16 (para Redis, Memcached, etc.)
$cache = new Psr16CacheAdapter($yourPsr16Cache, prefix: 'qvd:');

O LazyFileSession já utiliza cache interno em memória para symbol tables carregadas.

Facade Qvd::from()

Entry point estático para uso limpo:

use Qvd\Qvd;

$resultado = Qvd::from('vendas.qvd')
    ->select('Produto', 'Valor')
    ->whereBetween('Valor', [100, 5000])
    ->orderBy('Valor', 'desc')
    ->limit(50)
    ->get();

Uso em Laravel 12+

Instale com Composer e use a mesma API em controllers, jobs ou commands:

namespace App\Http\Controllers;

use Qvd\Qvd;

final class VendasController
{
    public function index()
    {
        $vendas = Qvd::from(storage_path('qvd/vendas.qvd'))
            ->select('Cliente', 'Produto', 'Valor')
            ->where('Valor', '>=', 100)
            ->orderBy('Valor', 'desc')
            ->limit(50)
            ->get();

        return response()->json($vendas);
    }
}

Requisitos: PHP ^8.3 (compatível com Laravel 12+).

Migração da API anterior

A versão 1.0 mantém backward compatibility. Mudanças:

Antes (v0.x) Agora (v1.0) Status
$reader->rows() $reader->cursor() rows() deprecated, funciona como alias
N/A $reader->get() Novo — materializa array
N/A $reader->first() / firstOrNull() Novo
N/A $reader->orderBy() / limit() Novo
N/A $reader->whereBetween() Novo
N/A Qvd::from() Novo facade

Se você usava rows(), ele continua funcionando (emite E_USER_DEPRECATED). Substitua por cursor() para silenciar.

Arquitetura Interna

QvdReader (fluent API)
  └── QueryState (imutável: select, where, orderBy, limit)
        └── QueryPlanner
              ├── ProjectionResolver (quais campos decodificar)
              ├── PredicatePushdownResolver (filtros no nível do symbol index)
              └── QueryPlan
                    └── StreamingExecutor
                          ├── LazyFileSession (seek-and-read, sem load total em memória)
                          ├── RowSorter (null-last)
                          └── FilterEvaluator (hash-set otimizado para whereIn)

Otimizações:

  • Predicate Pushdown: predicados = e IN com AND são resolvidos no nível da symbol table (antes de decodificar o registro completo)
  • Projection Pushdown: apenas campos necessários (select + where + orderBy) são decodificados
  • Hash Set: whereIn/whereNotIn usa hash set para membership check O(1)
  • Lazy File Access: LazyFileSession usa fseek/fread — nunca carrega o binário inteiro em memória
  • Null-last Sorting: nulls sempre ficam por último independente da direção

Desenvolvimento

composer install
composer verify     # PHPStan + PHPUnit
composer test       # apenas testes
composer analyse    # apenas análise estática

Benchmarks

composer require --dev phpbench/phpbench
vendor/bin/phpbench run benchmarks/ --report=aggregate

Licença

MIT