SELECT * FROM aluno?Ninguém. Até aqui o minidb é uma biblioteca: quem chama insere e le_pagina é o código de teste de vocês, e não uma consulta.
Hoje o minidb aprende a ler. Ao fim do M4, ele recebe SQL pela entrada padrão, responde, e o primeiro SELECT roda de verdade.
$ ./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.
Quebra o texto em tokens: palavra reservada, nome, número, texto, símbolo
Confere a ordem dos tokens contra a gramática e monta a árvore sintática
A tabela existe? A coluna existe? O tipo bate? Quem responde é o catálogo
No M4, um if por comando, direto sobre a árvore. No M5, operadores
Hoje, as etapas 1 e 2. Na próxima aula, a 3 e a 4. Cada etapa tem o seu tipo de erro, e misturar as três primeiras é o defeito mais comum deste módulo.
O primeiro comando que o minidb vai ler na vida:
CREATE TABLE aluno (id INT, matricula INT, nome CHAR(20));
create e CREATE dão o mesmo token20 já sai como número, e não como o texto "20"TABLE CREATE ( ( passa sem reclamarnome CHAR(20),
0 5 10 13
i | vê | decide | emite |
|---|---|---|---|
| 0 | n | letra: lê nome, que não é reservada | (NOME, "nome") |
| 4 | espaço | pula | nada |
| 5 | C | letra: lê CHAR, que está na lista | (PALAVRA, "CHAR") |
| 9 | ( | símbolo | (SIMBOLO, "(") |
| 10 | 2 | dígito: lê 20 e converte | (NUMERO, 20) |
| 12, 13 | ) , | símbolo, símbolo | (SIMBOLO, ")") (SIMBOLO, ",") |
| 14 | fim | acabou o texto | (FIM, None) |
[("NOME", "nome"), ("PALAVRA", "CHAR"), ("SIMBOLO", "("), ("NUMERO", 20),
("SIMBOLO", ")"), ("SIMBOLO", ","), ("FIM", None)]
A saída é uma lista de pares, em ordem. A ordem importa e há repetição, por isso não é um dicionário. O dicionário só aparece no parser.
nome CHAR(20)
("nome", "CHAR", 20)
Os mesmos 13 tokens, em duas ordens. Só o primeiro é válido:
CREATE TABLE aluno (id INT, nome CHAR(20)) CREATE TABLE aluno (id INT nome, CHAR(20))
O tokenizador olha um token de cada vez, sem lembrar o que veio antes. Para ele, os dois são iguais. Quem olha a ordem é o parser.
O parser confere a ordem contra a gramática e monta a estrutura: que coluna tem que tipo, que valor pertence a que WHERE.
minidbcomando := ( create | insert | select ) [ ; ]
create := CREATE TABLE nome ( coluna { , coluna } )
coluna := nome INT | nome CHAR ( número )
insert := INSERT INTO nome VALUES ( valor { , valor } )
select := SELECT ( * | nome { , nome } ) FROM nome [ WHERE nome = valor ]
valor := número | texto
[ ] é opcional, { } repete zero ou mais vezes, | é escolhaORDER BY, sem agregação, sem subconsulta, sem OR. É o suficiente para o critério de prontoCHAR(n) ocupa sempre n bytes. O registro continua de tamanho fixo, como no M1regra create := CREATE TABLE nome ( coluna { , coluna } ) vira função create(): espera CREATE espera TABLE tabela ← espera NOME espera ( colunas ← [ coluna() ] enquanto aceita , acrescenta coluna() em colunas espera ) devolve {op: CREATE, tabela, colunas}
espera consome o token ou dá erro de sintaxe; aceita consome só se casarespera, [ ] vira se aceita, { } vira enquanto aceita, | vira se/senão olhando o próximo tokencreate chama colunaespera FIM: se sobrou token, é erro20 é o tamanho de nome, e não uma colunaCREATE, TABLE, parênteses e vírgulas somem. Serviram para o parser decidir, e acabou{"op": "CREATE", "tabela": "aluno",
"colunas": [("id", "INT", 4), ("matricula", "INT", 4), ("nome", "CHAR", 20)]}
A árvore é o que o parser devolve: cada função de regra monta o seu pedaço, e a de cima junta tudo. Em Python, um dicionário basta. Em C, uma struct.
Diz o que o parser aceita
Recebe a lista de tokens, confere contra a gramática
O que a função devolve
SELECT nome FROM aluno WHERE id = 42
Tokenizador: nove tokens, contando o FIM
Parser: o 42 virou o valor do WHERE, e FROM e WHERE sumiram. A próxima aula executa árvores assim, começando pelo SELECT *. O WHERE fica para o M5.
minidb de vocêspager = Pager("dados.db") # M1
cache = Cache(pager) # M2
esquema = Esquema([("id", "INT", 4),
("matricula", "INT", 4)])
rid = insere(cache, primeira, esquema,
[1, 20260001])
O esquema e os valores estão escritos à mão no código.
A partir do M4, o esquema e os valores saem da árvore. Nada do que vocês escreveram é jogado fora: o parser só troca quem fornece os argumentos.
| A árvore diz | O minidb chama | Módulo |
|---|---|---|
"colunas": [("id", "INT", 4)] | Esquema(colunas), o mesmo do lab01b | M1 |
"valores": [1, 20260001] | esquema.serializa(valores) e insere | M1 |
"op": "SELECT" | percorrer os registros, como no lab01b | M1 |
| por baixo de tudo | cache.fixa(n) e cache.solta(n) | M2 |
"where": ("id", 42) | busca na árvore B+ | M3, ligada no M5 |
"tabela": "aluno" | em que página aluno começa? com quais colunas? | quinta |
A árvore traz o nome da tabela, mas o nome sozinho não diz onde ela mora. Quem sabe isso é o catálogo, assunto da próxima aula.
INSERT INTO aluno VALUES (3, 20260003, 'Carla');
Carla. O que muda nos tokens, e em que etapa o erro aparece?No minidb 0.0, Carla sem aspas funcionava. Por quê?
aluno e os três valores ficam. INSERT, INTO, VALUES, os parênteses, as vírgulas e o ; somem.
Sem aspas, Carla vira um token NOME, e o parser diz esperava valor, veio 'Carla'. É erro de sintaxe. O minidb 0.0 não tinha tokens: tudo era texto, e por isso nada era errado.
O parser decide o que é comando e o que é dado. Concatenar string apaga essa fronteira, e é por isso que a injeção existe.
Quem implementou um parser não esquece mais essa distinção. É o motivo de segurança ser ensinada aqui, e não numa aula separada.
Leitura obrigatória de três segundos: xkcd 327, Exploits of a Mom.
craftinginterpreters.comPróxima aula: o catálogo, e o primeiro SELECT * de verdade. Antes disso, o exercício de hoje
Para trazer no papel à próxima aula.
SELECT nome FROM aluno WHERE id == 42, SELECT * FROM WHERE id = 42, SELECT idade FROM aluno e SELECT * FROM aluno WHERE nome = 'O'Brien'"SELECT * FROM aluno WHERE nome = '" + nome + "'" e manda ao minidb, com nome = x' OR '1'='1. Liste os tokens. O que o minidb responde, e em que etapa? Ele está protegido?O item 2 não tem resposta de uma linha. Tragam o argumento.
def tokeniza(sql):
tokens, i = [], 0
while i < len(sql):
c = sql[i]
if c.isspace(): i += 1
elif c.isalpha(): # palavra reservada ou nome
j = i
while j < len(sql) and (sql[j].isalnum() or sql[j] == "_"): j += 1
p = sql[i:j]
tokens.append(("PALAVRA", p.upper()) if p.upper() in PALAVRAS else ("NOME", p))
i = j
elif c.isdigit(): ... # mesmo laço, com isdigit
elif c == "'": # texto vai até a próxima aspa
j = sql.find("'", i + 1)
if j < 0: raise ErroLexico(f"aspas sem fechamento na posição {i}")
tokens.append(("TEXTO", sql[i + 1:j]))
i = j + 1
elif c in "(),;*=":
tokens.append(("SIMBOLO", c)); i += 1
else: raise ErroLexico(f"caractere inesperado {c!r} na posição {i}")
return tokens + [("FIM", None)]
def create(self):
self.espera("PALAVRA", "CREATE")
self.espera("PALAVRA", "TABLE")
tabela = self.espera("NOME")
self.espera("SIMBOLO", "(")
colunas = [self.coluna()]
while self.aceita("SIMBOLO", ","):
colunas.append(self.coluna())
self.espera("SIMBOLO", ")")
return {"op": "CREATE", "tabela": tabela, "colunas": colunas}
def coluna(self):
nome = self.espera("NOME")
if self.aceita("PALAVRA", "INT"):
return (nome, "INT", 4)
self.espera("PALAVRA", "CHAR")
self.espera("SIMBOLO", "(")
n = self.espera("NUMERO")
self.espera("SIMBOLO", ")")
return (nome, "CHAR", n)
espera| e [ ] viram if{ } vira whilecreate chama colunaespera consome o token ou levanta erro de sintaxe. aceita consome só se casar, e diz se casou. As duas estão no próximo slide, e o select logo depois.
class Parser:
def __init__(self, tokens):
self.tokens, self.i = tokens, 0
def olha(self):
return self.tokens[self.i]
def aceita(self, tipo, valor):
if self.tokens[self.i] == (tipo, valor):
self.i += 1
return True
return False
def espera(self, tipo, valor=None):
t, v = self.tokens[self.i]
if t != tipo or (valor is not None and v != valor):
raise ErroSintatico(f"esperava {valor or tipo}, veio {v!r}")
self.i += 1
return v
def valor(self):
t, v = self.olha()
if t in ("NUMERO", "TEXTO"):
self.i += 1
return v
raise ErroSintatico(f"esperava valor, veio {v!r}")
def comando(self):
_, v = self.olha()
if v == "CREATE": arvore = self.create()
elif v == "INSERT": arvore = self.insert() # mesmo molde do create, com valor
elif v == "SELECT": arvore = self.select()
else: raise ErroSintatico(f"comando desconhecido {v!r}")
self.aceita("SIMBOLO", ";")
self.espera("FIM") # sobrou token? é erro
return arvore
def analisa(sql): # o que o resto do minidb chama
return Parser(tokeniza(sql)).comando()
SELECTdef select(self):
self.espera("PALAVRA", "SELECT")
if self.aceita("SIMBOLO", "*"):
colunas = ["*"]
else:
colunas = [self.espera("NOME")]
while self.aceita("SIMBOLO", ","):
colunas.append(self.espera("NOME"))
self.espera("PALAVRA", "FROM")
tabela = self.espera("NOME")
where = None
if self.aceita("PALAVRA", "WHERE"):
coluna = self.espera("NOME")
self.espera("SIMBOLO", "=")
where = (coluna, self.valor())
return {"op": "SELECT", "colunas": colunas, "tabela": tabela, "where": where}
O parser já aceita o WHERE. Quem vai executá-lo é o M5.
INSERTdef insert(self):
self.espera("PALAVRA", "INSERT")
self.espera("PALAVRA", "INTO")
tabela = self.espera("NOME")
self.espera("PALAVRA", "VALUES")
self.espera("SIMBOLO", "(")
valores = [self.valor()]
while self.aceita("SIMBOLO", ","):
valores.append(self.valor())
self.espera("SIMBOLO", ")")
return {"op": "INSERT", "tabela": tabela, "valores": valores}
O mesmo molde do create, com valor no lugar de coluna. O parser não confere se o valor bate com o tipo da coluna: isso é semântica, e precisa do catálogo.
import sys
def executa(sql, catalogo, cache):
arvore = analisa(sql) # M4, hoje
... # M4, parte 2: catálogo, M1 e M2
pager = Pager(sys.argv[1]) # M1
cache = Cache(pager) # M2
catalogo = Catalogo(cache) # M4, parte 2
for linha in sys.stdin:
if linha.strip():
print(executa(linha, catalogo, cache))
cache.descarrega() # M2: páginas sujas vão para o disco
É isso que transforma a biblioteca do M1 ao M3 num programa que lê SQL da entrada padrão.
# concatenação: o texto do usuário vira gramática sql = "SELECT * FROM aluno WHERE nome = '" + nome + "'" banco.execute(sql) # nome = x' OR '1'='1 → devolve a tabela inteira # parâmetro: a consulta é analisada antes, e o valor entra depois banco.execute("SELECT * FROM aluno WHERE nome = ?", (nome,)) # nome = x' OR '1'='1 → devolve nada
Para rodar: python3 injecao.py, em /assets/code/bd2-2026/. Só precisa do Python. Tente também python3 injecao.py "x' UNION SELECT 0, sql, 0 FROM sqlite_schema --".