SELECTNa terça, o parser transformou texto em árvore:
SELECT * FROM aluno;
# vira
{"op": "SELECT", "colunas": ["*"], "tabela": "aluno", "where": None}
aluno existe? Tem quais colunas? Mora em que página?
A árvore não sabe, e o parser não deve saber. Quem sabe é o catálogo, e hoje ele vai para o disco. Ao fim da aula, o SELECT * roda.
$ ./minidb dados.db CREATE TABLE aluno (id INT, matricula INT, nome CHAR(20)); OK INSERT INTO aluno VALUES (1, 20260001, 'Ana'); OK 1 INSERT INTO aluno VALUES (2, 20260002, 'Bruno'); OK 1 ^D $ ./minidb dados.db # outro processo SELECT * FROM aluno; 1|20260001|Ana 2|20260002|Bruno (2 linhas)
O segundo processo não viu o CREATE TABLE. Ele sabe que aluno existe, e com quais colunas, porque leu isso do disco. É a demonstração 4 do minidb 0.0, consertada.
Catálogo é o conjunto de tabelas em que o banco guarda a descrição das outras tabelas.
pg_class, pg_attribute, pg_index, consultáveis com SQLO SQLite vai além: guarda o próprio texto do CREATE TABLE, e analisa de novo toda vez que abre o arquivo.
sqlite> SELECT name, rootpage, sql FROM sqlite_schema;
aluno|2|CREATE TABLE aluno (id INT, nome TEXT, nota INT)
minidb é uma tabelaregistro do catálogo, 240 bytes, 17 por página nome CHAR(32) "aluno" primeira INT 2 // primeira página da tabela raiz INT 0 // raiz do índice, 0 se não há (M5) sql CHAR(200) "CREATE TABLE aluno (id INT, matricula INT, nome CHAR(20))"
CREATE TABLE é só um INSERT nessa tabela, mais uma página nova para a tabela criadaO catálogo descreve todas as tabelas. Quem descreve o catálogo?
ESQUEMA_CATALOGO = Esquema([("nome", "CHAR", 32), ("primeira", "INT", 4),
("raiz", "INT", 4), ("sql", "CHAR", 200)])
PAGINA_CATALOGO = 1 # fixa, combinada no código
class Catalogo:
def __init__(self, cache):
self.tabelas = {}
for nome, primeira, raiz, sql in varre(cache, PAGINA_CATALOGO, ESQUEMA_CATALOGO):
colunas = analisa(sql)["colunas"] # o parser, de novo
self.tabelas[nome] = (primeira, Esquema(colunas))
Só duas coisas ficam no código: o esquema do próprio catálogo, e a página onde ele começa. Todo o resto vem do disco.
dados.db
+-----------+-----------+-----------+-----------+-----------+
| página 0 | página 1 | página 2 | página 3 | página 4 |
| metadados | catálogo | aluno | curso | aluno |
+-----------+-----------+-----------+-----------+-----------+
| ^
+--- próxima = 4 -------+ // a 4 aponta para 0: fim
O heap do M1 supunha uma tabela só. Agora cada página sabe qual é a próxima da mesma tabela, e o cabeçalho de 16 bytes ainda tem espaço para isso:
byte 0-1 n_slots ocupados byte 2-3 tamanho do registro byte 4-7 número desta página byte 8-11 próxima página da mesma tabela // novo. 0 é o fim, porque a 0 nunca é dado byte 12-15 reservado // o M7 ainda cabe aqui
hexdump$ hexdump -C -s 4096 -n 96 dados.db 00001000 01 00 f0 00 01 00 00 00 00 00 00 00 00 00 00 00 |................| 00001010 61 6c 75 6e 6f 00 00 00 00 00 00 00 00 00 00 00 |aluno...........| 00001020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00001030 02 00 00 00 00 00 00 00 43 52 45 41 54 45 20 54 |........CREATE T| 00001040 41 42 4c 45 20 61 6c 75 6e 6f 20 28 69 64 20 49 |ABLE aluno (id I| 00001050 4e 54 2c 20 6d 61 74 72 69 63 75 6c 61 20 49 4e |NT, matricula IN|
00001000 é 4096, a página 1. Um slot ocupado, registro de 0xf0 = 240 bytes, página número 1, sem próxima00001010 a 0000102f, os 32 bytes do nome. Em 00001030, primeira = 200001038 começa o texto do CREATE TABLE, que o parser vai ler de voltadef varre(cache, primeira, esquema):
n = primeira
while n != 0:
buf = cache.fixa(n)
try:
n_slots, tam, _, proxima = struct.unpack_from("<HHII", buf, 0)
for s in range(n_slots):
yield esquema.desserializa(buf, TAM_CABECALHO + s * tam)
finally:
cache.solta(n)
n = proxima
fixa e solta do M2, com try/finallydef executa(sql, catalogo, cache):
arvore = analisa(sql) # tokenizador e parser
if arvore["op"] == "CREATE":
catalogo.cria(arvore["tabela"], arvore["colunas"], sql)
return "OK"
primeira, esquema = catalogo.busca(arvore["tabela"]) # semântica: a tabela existe?
if arvore["op"] == "INSERT":
confere_tipos(esquema, arvore["valores"]) # semântica: os tipos batem?
insere(cache, primeira, esquema, arvore["valores"])
return "OK 1"
linhas = ["|".join(map(str, r)) for r in varre(cache, primeira, esquema)]
n = len(linhas)
return "\n".join(linhas + [f"({n} linha{'s' * (n != 1)})"])
É um interpretador de árvore: olha o op e faz. Funciona para SELECT *. No M5 isso vira uma pilha de operadores, e é ali que entram o WHERE e o índice.
minidbSELECT * FROM aluno; M4 tokenizador 5 tokens, mais o FIM M4 parser {"op": "SELECT", "colunas": ["*"], "tabela": "aluno", ...} M4 catálogo aluno começa na página 2, registro de 28 bytes M2 cache de páginas entrega as páginas da corrente, da memória ou do disco M1 página e arquivo lê 4096 bytes no deslocamento certo M4 execução desserializa cada slot e imprime 1|20260001|Ana M3 árvore B+ ainda parada. Quem decide usar o índice é o M5
O parser não sabe o que é página, e o cache não sabe o que é SQL. Cada módulo só usa o que vem antes dele.
$ python3 injecao.py "x' UNION SELECT 0, sql, 0 FROM sqlite_schema --" concatenado: SELECT * FROM aluno WHERE nome = 'x' UNION SELECT 0, sql, 0 FROM sqlite_schema --' devolveu: [(0, 'CREATE TABLE aluno (id INT, nome TEXT, nota INT)', 0)]
-- transforma a aspa que sobrou em comentárioUNION junta uma consulta inteira, escolhida pelo atacanteSELECT * de ponta a pontaWHERE, índice e operadoreskill -9No laboratório: primeiro o tokenizador e o parser, depois o catálogo e o SELECT *. No NOTES.md, digam onde o catálogo mora e por quê.
sqlite.org/schematab.htmlpg_class e companhiaPróximo: laboratório do M4. Antes disso, o exercício de hoje
Para trazer no papel ao laboratório.
curso (id INT, nome CHAR(40)), criada logo depois de aluno. Em que página ela começa, e quantos bytes tem cada registro dela?dados.db começa o texto do CREATE TABLE de curso? Confira com o hexdumpt (a INT, b INT) recebe 1200 linhas. Quantas páginas tem a corrente dela, e quantas páginas SELECT * FROM t lê, contando o catálogo?O item 2 é a conta do lab01, com o catálogo no lugar do registro.
def nova_pagina(cache, tam_reg):
n = cache.pager.aloca()
buf = cache.fixa(n)
struct.pack_into("<HHII", buf, 0, 0, tam_reg, n, 0) # 0 slots, sem próxima
cache.solta(n, sujou=True)
return n
class Catalogo: # continua o __init__ da aula
def cria(self, nome, colunas, sql):
if nome in self.tabelas:
raise ErroSemantico(f"tabela {nome} já existe")
esquema = Esquema(colunas)
primeira = nova_pagina(self.cache, esquema.tamanho)
insere(self.cache, PAGINA_CATALOGO, ESQUEMA_CATALOGO, [nome, primeira, 0, sql])
self.tabelas[nome] = (primeira, esquema)
def busca(self, nome):
if nome not in self.tabelas:
raise ErroSemantico(f"tabela {nome} não existe")
return self.tabelas[nome]
def insere(cache, primeira, esquema, valores):
reg = esquema.serializa(valores)
n = primeira
while True:
buf = cache.fixa(n)
n_slots, tam, num, proxima = struct.unpack_from("<HHII", buf, 0)
if TAM_CABECALHO + (n_slots + 1) * tam <= TAM_PAGINA: # cabe aqui
off = TAM_CABECALHO + n_slots * tam
buf[off:off + tam] = reg
struct.pack_into("<H", buf, 0, n_slots + 1)
cache.solta(n, sujou=True)
return (n, n_slots) # o RID
if proxima == 0: # fim da corrente
proxima = nova_pagina(cache, tam)
struct.pack_into("<I", buf, 8, proxima) # bytes 8-11
cache.solta(n, sujou=True)
else:
cache.solta(n)
n = proxima
Percorrer a corrente a cada inserção custa caro em tabela grande. Guardar a última página no catálogo resolve. Decisão para o NOTES.md.
INSERTdef confere_tipos(esquema, valores):
if len(valores) != len(esquema.colunas):
raise ErroSemantico(f"esperava {len(esquema.colunas)} valores, vieram {len(valores)}")
for (nome, tipo, n), v in zip(esquema.colunas, valores):
if tipo == "INT" and not isinstance(v, int):
raise ErroSemantico(f"coluna {nome} é INT, veio {v!r}")
if tipo == "CHAR" and not (isinstance(v, str) and len(v.encode()) <= n):
raise ErroSemantico(f"coluna {nome} é CHAR({n}), veio {v!r}")
Fica fora do parser: ele só sabe que veio um número ou um texto, e não qual coluna espera o quê.