caiopinheirom / php-qvd
Streaming QVD reader for PHP 8.3+
Requires
- php: ^8.3
Requires (Dev)
- phpbench/phpbench: ^1.3
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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
=eINcom 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/whereNotInusa hash set para membership check O(1) - Lazy File Access:
LazyFileSessionusafseek/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