Banco de Dados II — 2026.2

Laboratório M1 · parte 1
Leitura e escrita de páginas

👨‍🏫 Prof. Gustavo Pinto 🏛️ UFPA 📅 15 de setembro de 2026 🕐 7h30 – 9h

De onde a gente parou

Em 03/09 vocês saíram com três tarefas.

Hoje a gente confere esse número, conserta o que estiver torto e sobe o nível da implementação. Abram o código agora.

A conta que todo mundo tinha que fazer

Página de 4096 bytes, cabeçalho de 16, registro de 8. O registro no slot 0 da página 2 começa em que byte?

página 2  →  2 × 4096            = 8192   // início da página
cabeçalho →  8192 + 16            = 8208   // início do slot 0
slot 0    →  8208 + 0 × 8        = 8208
registro  →  bytes 8208 a 8215     // 8 bytes, id e matrícula

Onde cada página começa

página 2  (bytes 8192 a 12287)
+------------+--------+--------+--------+-----+--------+-----------+
| cabeçalho  | slot 0 | slot 1 | slot 2 | ... | slot n | livre     |
+------------+--------+--------+--------+-----+--------+-----------+
  8192         8208     8216     8224                    até 12287
   16 B         8 B      8 B      8 B         8 B

A fórmula, uma vez só

deslocamento(pagina, slot) = pagina × TAM_PAGINA
                           + TAM_CABECALHO
                           + slot × TAM_REGISTRO

O contrato das duas funções

le_pagina(n)

Devolve exatamente 4096 bytes da página n, ou falha com erro claro.

Nunca devolve 3000 bytes. Nunca devolve vazio. Nunca devolve um pedaço da página vizinha.

escreve_pagina(n, buf)

Grava exatamente 4096 bytes na página n, ou falha com erro claro.

Recebeu buffer de outro tamanho, é erro de programação. Falhe alto, e não ajuste em silêncio.

O resto do semestre chama essas duas funções. Implemente com cuidado!

A implementação mínima

Em Python, com o arquivo já aberto em r+b.

TAM_PAGINA = 4096

def le_pagina(f, n):
    f.seek(n * TAM_PAGINA)
    return f.read(TAM_PAGINA)

def escreve_pagina(f, n, buf):
    f.seek(n * TAM_PAGINA)
    f.write(buf)

É um bom lugar para começar, e um lugar ruim para parar.

Isso funciona mesmo?

Antes de seguir, rodem esses dez segundos de teste na implementação que vocês acabaram de ver.

f = open("teste.db", "w+b")
escreve_pagina(f, 2, b"A" * 4096)
escreve_pagina(f, 3, b"B" * 4096)
f.close()                              # fecha de verdade

f = open("teste.db", "r+b")            # reabre: o disco é a prova
p2 = le_pagina(f, 2)
print(len(p2), p2 == b"A" * 4096)      # ida e volta
print(le_pagina(f, 3)[:1])             # a vizinha continua intacta
print(os.path.getsize("teste.db") % 4096 == 0)
4096 True
b'B'
True

Deu isso nas três linhas? Então a mínima está de pé. Agora reparem no que esse teste não perguntou.

Estudem o hexdump

É assim que vocês olham o arquivo sem passar pelo próprio código. Um exemplo mínimo, depois de gravar o registro (1, 20260001):

$ hexdump -C -s 8192 -n 32 dados.db
             pula 8192 bytes   mostra 32
00002000  01 00 08 00 02 00 00 00  00 00 00 00 00 00 00 00
00002010  01 00 00 00 a1 24 35 01  00 00 00 00 00 00 00 00

O que a mínima assume calada

Nenhuma das quatro é verdade. Vamos uma por uma.

A implementação adequada

Um objeto que guarda o arquivo aberto e o número de páginas. Metade das linhas é validação, e é de propósito.

class Pager:
    def __init__(self, caminho):
        novo = not os.path.exists(caminho)
        self.f = open(caminho, "w+b" if novo else "r+b")
        self.f.seek(0, os.SEEK_END)
        self.n_paginas = self.f.tell() // TAM_PAGINA

    def le(self, n):
        self._valida(n)
        self.f.seek(n * TAM_PAGINA)
        buf = self.f.read(TAM_PAGINA)
        if len(buf) != TAM_PAGINA:
            raise IOError(f"página {n} truncada: {len(buf)} bytes")
        return bytearray(buf)

A implementação adequada

    def escreve(self, n, buf):
        self._valida(n)
        if len(buf) != TAM_PAGINA:
            raise ValueError(f"página com {len(buf)} bytes")
        self.f.seek(n * TAM_PAGINA)
        self.f.write(buf)

    def aloca(self):
        n = self.n_paginas
        self.f.seek(n * TAM_PAGINA)
        self.f.write(bytes(TAM_PAGINA))   # 4096 zeros
        self.n_paginas += 1
        return n

    def sync(self):
        self.f.flush()
        os.fsync(self.f.fileno())

Um cabeçalho que não trava vocês no M7

São 16 bytes. Usem 8 agora e deixem 8 reservados, porque mudar o formato no meio do semestre custa remontar todos os arquivos de teste.

byte 0-1    n_slots ocupados        // o que o M1 precisa
byte 2-3    tamanho do registro     // valida o arquivo na abertura
byte 4-7    número desta página     // detecta página trocada de lugar
byte 8-15   reservado               // o LSN do M7 mora aqui

Página 0: os metadados

byte 0-7    "MINIDB\0"              // número mágico
byte 8-9    versão do formato       // 1
byte 10-13  tamanho da página       // 4096
byte 14-17  total de páginas
byte 18-21  primeira página com espaço livre

Se vocês quiserem ir além

pread e pwrite

Leem e escrevem em um deslocamento sem mexer na posição do arquivo. Some o seek, some a classe inteira de bugs em que duas chamadas se atrapalham.

os.pread(fd, n, off) em Python, pread(2) em C

Endianness explícito

Gravem com struct.pack("<II", ...), e não com o que a máquina achar. O arquivo precisa ser lido na máquina do colega.

Bancos de verdade fixam isso no formato, e o seu deve fixar também

bytearray e memoryview

Trabalhem a página como buffer mutável em vez de recortar e concatenar bytes. No M2 a mesma página vai ser alterada muitas vezes antes de descer para o disco.

Soma de verificação

Quatro bytes de crc32 no cabeçalho detectam página escrita pela metade. É como o PostgreSQL percebe corrupção.

Como saber que está certo

O teste 3 é o que pega erro de seek. Escrevam ele hoje.

Agora, mão na massa

Primeiros 30 minutos

conferir
  • Rodar o hexdump e achar o byte 8208
  • Corrigir a fórmula, se for o caso
  • Extrair o deslocamento para uma função

30 minutos seguintes

subir o nível
  • Criar o arquivo quando não existir
  • aloca() e contagem de páginas
  • Validar tamanho do buffer e número da página

Últimos 30 minutos

provar
  • Os cinco testes do slide anterior
  • Cabeçalho de 16 bytes escrito e lido
  • Commit, com mensagem que diz o que mudou

Quinta, 17/09

Entrega do M1 em 17/09, às 23h59. Pull request, testes de aceitação passando, NOTES.md com a decisão mais difícil do módulo, e histórico de commits que mostre o trabalho acontecendo.

Dúvidas?

E, antes de sair, o exercício desta aula

📧 gpinto@ufpa.br

Exercício de saída

Antes de sair, um teste que o script de aceitação não faz por vocês.

Esse parágrafo é o começo do M7. Quem entende isso hoje economiza uma semana em novembro.