Dia 8 — MySQL: o banco relacional

Informatica · Conteudo · publicado em 30/09/2026
Dia 8 de 16

MySQL: o banco relacional

Aula 1

Banco de dados e MySQL

Banco de dados e MySQL

Banco de dados é o software que guarda dado de forma que ele sobreviva ao fim do

processo. Dados persistentes é o nome desse tipo de dado: o que está gravado

continua lá quando o Node desliga e volta. MySQL é um deles: um SGBD —

sistema gerenciador de banco de dados.

A coisa que mais confunde quem vem do navegador é que MySQL **não é uma

biblioteca. É um servidor**, que roda em uma máquina e escuta em uma porta —

o servidor de banco. O mysql2 não guarda nada: ele é o telefone, e o Node é

quem faz a ligação.

Node (cliente)  --TCP-->  porta 3306  -->  MySQL (servidor)  -->  disco

Por isso dois fatos que parecem detalhe são estruturais:

  • Se o servidor não está no ar, a conexão dá ECONNREFUSED — e o erro não é do

Node nem do driver.

  • A porta 3306 é do servidor. O mysql2 pergunta por ela, e em banco

remoto a porta pode ser outra.

O que perguntar ao banco antes de afirmar qualquer coisa

const [v] = await conexao.query('SELECT VERSION() AS versao');
const [i] = await conexao.query('SELECT DATABASE() AS banco, USER() AS usuario');

VERSION() devolve a versão da máquina que rodou — pode ser outra na máquina

de quem lê. DATABASE() e USER() respondem a pergunta que importa em

depuração: "o Node está falando com quem?".

@@port, @@hostname e @@max_connections dizem do lado do servidor. @@port

é a porta que o servidor escuta; a que o cliente procurou é outra informação.

NOW() é o relógio do banco, não da máquina — em servidor de produção, a

diferença entre o relógio do Node e o do banco é motivo de bug em relatório.

SHOW: o comando de administração

A linguagem SQL é a língua que o servidor de banco entende, e ela não é

JavaScript: é declarativa. O que se escreve é o resultado que se quer, e quem

decide como chegar nele é o SGBD — o Node diz "quero estes registros", não "faça

isto passo a passo".

SHOW não faz parte do SQL padrão. É comando de MySQL/MariaDB, e é a forma

curta de olhar a estrutura sem lembrar o nome dela.

ComandoO que mostra
SHOW TABLEStabelas do banco conectado
SHOW DATABASESbancos visíveis para o usuário
SHOW CREATE TABLE to SQL que recria a tabela
SHOW COLUMNS FROM tcolunas, tipos e chaves
SHOW INDEX FROM tíndices
SHOW GRANTSprivilégios do usuário atual
SHOW STATUS / SHOW VARIABLESo que o servidor está fazendo agora

SHOW GRANTS e a permissão mínima

const [g] = await conexao.query('SHOW GRANTS FOR CURRENT_USER()');

CURRENT_USER() dispensa saber o nome do usuário. É a forma de responder "por que

o INSERT deu denied?" quando o usuário existe mas não tem o privilégio — e de

verificar, num usuário de teste, que ele só enxerga o banco de teste e **não

alcança produção**. Note que a linha de privilégio vem com IDENTIFIED BY PASSWORD

e um hash: mesmo o SHOW GRANTS não devolve a senha, e nenhum exemplo deste

material imprime essa parte.

information_schema: o catálogo do banco

Existe um schema virtual chamado information_schema: não está no disco, e o

servidor monta na hora a partir do catálogo interno. Ele é a forma programática de

perguntar "que tabelas existem?" sem saber os nomes.

await conexao.query(
  `SELECT COUNT(*) AS n FROM information_schema.tables WHERE table_schema = ?`,
  [process.env.DB_NAME]
);

O SHOW TABLES é a forma curta de consultar a mesma informação; information_schema

é a forma que entra em código, porque aceita filtro.

Docker e container de banco

A forma mais rápida de ter um MySQL na máquina de estudo é um container: a

imagem vem com o servidor pronto, e o banco vive num volume nomeado que

sobrevive ao docker stop. Os dois comandos de quem está começando:

docker run -d --name banco -e MYSQL_ROOT_PASSWORD=SUA_SENHA -p 3306:3306 mysql:8

A senha aqui é de ambiente e é fictícia; em máquina de verdade ela vai para o

.env (aula 2 do dia 2), nunca para o histórico do shell. E a porta -p 3306:3306

é o mapeamento: a 3306 da esquerda é na máquina, a da direita é no container —

é a diferença entre @@port e o que o cliente procura.

Exemplo

'use strict';

// Exemplo da aula 1 do dia 8: o que existe do outro lado da porta 3306.
//
// A aula e sobre MySQL, e a tentacao e escrever a versao no texto. O exemplo faz
// o contrario: ele PERGUNTA ao banco quem ele e, de onde veio, quantos schemas
// ele enxerga e o que o usuario tem permissao de fazer. Tudo o que aparece na
// saida saiu de uma consulta — a maquina de quem le pode devolver outro numero, e
// a pagina nao pode afirmar um.
//
// O que fica provado: MySQL e um SERVIDOR, nao uma biblioteca. Node fala com
// ele por TCP, o cliente (`mysql2`) e so o telefone. E o `SHOW GRANTS` mostra que
// o usuario do exemplo so enxerga o banco de teste — nenhum exemplo deste
// material escreve fora de `DB_NAME`.

const mysql = require('mysql2/promise');

async function main() {
  // `createConnection` abre o socket TCP e espera o servidor responder. Sem o
  // `await`, a primeira consulta correria contra uma conexao que ainda nao
  // existe.
  const conexao = await mysql.createConnection({
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    user: process.env.DB_USER,
    password: process.env.DB_PASS,     // do ambiente, nunca escrita aqui
    database: process.env.DB_NAME,
  });

  try {
    console.log('--- quem e este banco ---');

    // `SELECT VERSION()` e a forma de perguntar a versao. A versao e a da
    // maquina que rodou, e pode ser outra na maquina de quem le.
    const [versao] = await conexao.query('SELECT VERSION() AS versao');
    console.log('versao:', versao[0].versao);

    // `SELECT DATABASE()` e `USER()` dizem onde estou e quem sou. Sao as duas
    // perguntas que respondem "o Node esta falando com quem?".
    const [identidade] = await conexao.query(
      'SELECT DATABASE() AS banco, USER() AS usuario, CURRENT_USER() AS autenticado, CONNECTION_ID() AS conexao_id'
    );
    console.log('banco:', identidade[0].banco);
    console.log('usuario:', identidade[0].usuario, '| autenticado como:', identidade[0].autenticado);
    console.log('id desta conexao no servidor:', identidade[0].conexao_id);
    console.log('(o id muda a cada execucao: e a conexao nova que o exemplo abriu agora)');

    // --- o que o cliente e o que o servidor sao ---
    //
    // `@@port` e a porta que o SERVIDOR escuta; `process.env.DB_PORT` e a porta
    // que o CLIENTE procurou. Sao numeros iguais quando o banco e local, e
    // diferentes quando o cliente vem de outra maquina.
    //
    // `@@port` e a forma abreviada que o SQL usa; em `SELECT @@port AS x` o
    // driver pode entregar a coluna como `port`, e o alias e o que define o nome
    // que chega em Node. Por isso o exemplo usa as funcoes de servidor, cujo nome
    // ja vem completo e nao depende do alias.
    const [portas] = await conexao.query(
      `SELECT @@hostname AS maquina, @@max_connections AS max_conexoes,
              @@port AS porta_servidor, @@sql_mode AS modo_sql`
    );
    console.log('');
    console.log('--- cliente e servidor ---');
    console.log('cliente procurou:', process.env.DB_HOST + ':' + process.env.DB_PORT);
    console.log('servidor escuta em:', portas[0].porta_servidor, '| maquina:', portas[0].maquina);
    console.log('conexoes simultaneas que o servidor aceita:', portas[0].max_conexoes);
    console.log('o Node e o cliente; o MySQL e o servidor. O `mysql2` e o telefone entre os dois.');
    console.log('');
    console.log('sql_mode:', portas[0].modo_sql);
    console.log('`sql_mode` define o que o SQL aceita: se a data entra como');
    console.log('"2026-05-04" ou so como "2026-05-04 00:00:00", se o silencio vira erro.');
    console.log('Duas maquinas com modos diferentes aceitam INSERTs diferentes, e por');
    console.log('isso que o exemplo LÊ o modo em vez de afirmar qual ele e.');

    // --- os schemas visiveis ---
    //
    // `SHOW DATABASES` lista o que o usuario enxerga. Um usuario com privilegio
    // minimo ve so o banco em que tem coisa — e ver so o banco de teste e a
    // prova de que nenhum exemplo deste material toca no banco de producao.
    const [schemas] = await conexao.query('SHOW DATABASES');
    console.log('');
    console.log('--- bancos visiveis para este usuario ---');
    console.log(schemas.map((l) => '  ' + Object.values(l)[0]).join('\n'));
    console.log('o usuario do material so enxerga o banco de teste.');

    // --- os privilegios: o que este usuario pode fazer ---
    //
    // `SHOW GRANTS FOR CURRENT_USER()` mostra a permissao sem precisar saber o
    // nome do usuario. E a forma de responder "por que o INSERT deu denied?"
    // quando o usuario existe mas nao tem o privilegio.
    //
    // A linha vem com `IDENTIFIED BY PASSWORD` e um hash. O exemplo corta essa
    // parte ANTES de imprimir: e o proprio `SHOW GRANTS` que nao devolve a
    // senha, e ainda assim a saida vira pagina publica.
    const [grants] = await conexao.query('SHOW GRANTS FOR CURRENT_USER()');
    console.log('');
    console.log('--- privilegios (SHOW GRANTS) ---');
    for (const linha of grants) {
      console.log('  ' + Object.values(linha)[0].split(' IDENTIFIED BY')[0]);
    }
    console.log('ALL PRIVILEGES so no banco de teste: nenhum exemplo escreve fora de DB_NAME.');
    console.log('(a parte do hash foi cortada antes de imprimir: a saida vira pagina)');

    // --- SQL e o que ele e ---
    //
    // `SELECT 1` e o "ping" do SQL: prova que o interpretador respondeu sem
    // tocar em tabela nenhuma. O driver manda texto e o banco devolve o que
    // aquele texto quer dizer.
    const [ping] = await conexao.query(
      "SELECT 1 AS ok, 'texto' AS literal, NOW() AS agora_no_banco"
    );
    console.log('');
    console.log('--- o que o SQL devolve ---');
    console.log('SELECT 1      ->', ping[0].ok, '(numero)');
    console.log("SELECT 'texto' ->", ping[0].literal, '(texto)');
    console.log('NOW()         ->', ping[0].agora_no_banco.toISOString());
    console.log('o relogio vem do SERVIDOR, nao da maquina: e o banco que fala que horas sao.');

    // --- o catalogo: onde o banco guarda a si mesmo ---
    //
    // `information_schema` e um schema virtual: nao existe no disco, e o
    // servidor monta na hora a partir do catalogo interno. Consultar ele e o
    // jeito de perguntar "que tabelas existem?" sem saber os nomes.
    const [catalogo] = await conexao.query(
      `SELECT COUNT(*) AS tabelas
         FROM information_schema.tables
        WHERE table_schema = ?`, [process.env.DB_NAME]
    );
    const [colunas] = await conexao.query(
      `SELECT COUNT(*) AS colunas
         FROM information_schema.columns
        WHERE table_schema = ?`, [process.env.DB_NAME]
    );
    console.log('');
    console.log('--- o catalogo (information_schema) ---');
    console.log('tabelas no banco de teste:', catalogo[0].tabelas);
    console.log('colunas no banco de teste:', colunas[0].colunas);
    console.log('o catalogo e virtual: o servidor monta na consulta, e `SHOW TABLES`');
    console.log('e a forma curta de consultar a mesma informacao.');

    // --- SHOW, o comando de administracao ---
    console.log('');
    console.log('--- SHOW: o comando de administracao ---');
    console.log('SHOW TABLES             | tabelas do banco conectado');
    console.log('SHOW DATABASES          | bancos visiveis');
    console.log('SHOW CREATE TABLE t     | o SQL que recria a tabela');
    console.log('SHOW COLUMNS FROM t     | colunas, tipos e chaves');
    console.log('SHOW INDEX FROM t       | indices');
    console.log('SHOW GRANTS             | privilegios do usuario');
    console.log('SHOW STATUS / VARIABLES | o que o servidor esta fazendo agora');
  } finally {
    // `end()` fecha a conexao. Sem ele o socket segura o event loop e o
    // processo nao termina sozinho.
    await conexao.end();
    console.log('');
    console.log('conexao encerrada com end().');
  }
}

main().catch((erro) => {
  console.error('falhou:', erro.code || erro.name, '-', erro.message);
  process.exit(1);
});

Saída real

--- quem e este banco ---
versao: 10.11.14-MariaDB-0ubuntu0.24.04.1
banco: materiais_teste
usuario: materiais@localhost | autenticado como: materiais@localhost
id desta conexao no servidor: 7306
(o id muda a cada execucao: e a conexao nova que o exemplo abriu agora)

--- cliente e servidor ---
cliente procurou: 127.0.0.1:3306
servidor escuta em: 3306 | maquina: vmi3339533
conexoes simultaneas que o servidor aceita: 151
o Node e o cliente; o MySQL e o servidor. O `mysql2` e o telefone entre os dois.

sql_mode: IGNORE_SPACE,STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
`sql_mode` define o que o SQL aceita: se a data entra como
"2026-05-04" ou so como "2026-05-04 00:00:00", se o silencio vira erro.
Duas maquinas com modos diferentes aceitam INSERTs diferentes, e por
isso que o exemplo LÊ o modo em vez de afirmar qual ele e.

--- bancos visiveis para este usuario ---
  information_schema
  materiais_teste
o usuario do material so enxerga o banco de teste.

--- privilegios (SHOW GRANTS) ---
  GRANT USAGE ON *.* TO `materiais`@`localhost`
  GRANT ALL PRIVILEGES ON `materiais_teste`.* TO `materiais`@`localhost`
ALL PRIVILEGES so no banco de teste: nenhum exemplo escreve fora de DB_NAME.
(a parte do hash foi cortada antes de imprimir: a saida vira pagina)

--- o que o SQL devolve ---
SELECT 1      -> 1 (numero)
SELECT 'texto' -> texto (texto)
NOW()         -> 2026-09-30T15:15:35.000Z
o relogio vem do SERVIDOR, nao da maquina: e o banco que fala que horas sao.

--- o catalogo (information_schema) ---
tabelas no banco de teste: 86
colunas no banco de teste: 333
o catalogo e virtual: o servidor monta na consulta, e `SHOW TABLES`
e a forma curta de consultar a mesma informacao.

--- SHOW: o comando de administracao ---
SHOW TABLES             | tabelas do banco conectado
SHOW DATABASES          | bancos visiveis
SHOW CREATE TABLE t     | o SQL que recria a tabela
SHOW COLUMNS FROM t     | colunas, tipos e chaves
SHOW INDEX FROM t       | indices
SHOW GRANTS             | privilegios do usuario
SHOW STATUS / VARIABLES | o que o servidor esta fazendo agora

conexao encerrada com end().
Aula 2

Criar banco e tabela

Criar banco e tabela

CREATE DATABASE cria o schema e USE escolhe qual está em uso. A partir daí,

CREATE TABLE desenha a tabela — e o desenho é a decisão mais importante do

banco, porque é o que os outros quarenta exemplos vão ter de respeitar.

CREATE DATABASE materiais_teste;
USE materiais_teste;

CREATE TABLE tb_aluno (
  id        INT AUTO_INCREMENT PRIMARY KEY,
  nm_aluno  VARCHAR(40)  NOT NULL,
  email     VARCHAR(80)  NULL,
  turma     INT          NOT NULL,
  dt_cadastro DATE       NOT NULL,
  vl_nota   DECIMAL(4,2) NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

As restrições, e o que cada uma garante

O tipo de dado é a escolha de cada coluna, e ele responde a duas perguntas: o

que o valor significa e quanto espaço ele ocupa. INT é número inteiro,

VARCHAR(40) é texto de até 40 caracteres, DATE é data sem hora — e o banco

passa a recusar o que não cabe antes de a consulta chegar ao Node.

RestriçãoO que garante
PRIMARY KEYvalor único e nunca nulo; é a chave que o índice usa
AUTO_INCREMENTo banco numera sozinho, sequencial, sem repetir
NOT NULLa coluna não aceita NULL
DEFAULTvalor usado quando o INSERT não manda o campo
UNIQUEvalor único, diferente da chave primária
ENGINE=InnoDBtransação e chave estrangeira; o padrão desde o MySQL 5.5
DEFAULT CHARSET=utf8mb4guarda o emoji e qualquer caractere Unicode

AUTO_INCREMENT só funciona em coluna indexada. Pedir

AUTO_INCREMENT sem chave dá erro na hora do CREATE TABLE, e não na hora do

INSERT — o que confunde quem esperava o erro depois.

IF NOT EXISTS e o motivo de o material não usar DROP

CREATE TABLE IF NOT EXISTS não reclama se a tabela já existe. DROP TABLE apaga

a tabela e tudo dentro, e não é usado em nenhum exemplo do material — não por

medo, por repetibilidade: todo exemplo roda duas vezes seguidas e precisa dar a

mesma saída, e DROP + CREATE faria a segunda rodada perder o que a primeira

gravou.

A combinação padrão é CREATE TABLE IF NOT EXISTS na estrutura e TRUNCATE no

começo do exemplo, nunca no fim: a aula seguinte precisa achar a tabela no

estado que a anterior deixou.

SHOW CREATE TABLE e SHOW COLUMNS

SHOW CREATE TABLE tb_aluno;   -- o SQL que recria a tabela
SHOW COLUMNS FROM tb_aluno;   -- colunas, tipos, nulabilidade, chave

SHOW CREATE TABLE é a forma de conferir o desenho depois de criado, e é nele que

aparecem as normalizações que o pedido fez: o NOT NULL vira atributo de coluna,

o AUTO_INCREMENT vira atributo de tabela, o VARCHAR(40) foi guardado como

varchar(40). Note que o servidor acrescenta o que foi omitido — aqui o

COLLATE=utf8mb4_general_ci, que ninguém pediu.

Em SHOW COLUMNS, o Default de uma coluna NOT NULL aparece como null. Isso

não é a mesma coisa que "aceita nulo": a coluna que aceita NULL é a que mostra

Null: YES, e a que mostra Null: NO com Default: null é obrigatória e sem

padrão — escrever nela sem valor dá ER_BAD_NULL_ERROR.

O Node manda um valor; o banco guarda um tipo

A distinção que a leitura de volta prova:

O Node mandaO banco guardaVolta em Node
'Ana'varchar(40)string
3intnumber
'2026-05-04'dateobjeto Date
8.5decimal(4,2)string "8.50"

DECIMAL volta como string porque é exato, e é por isso que dinheiro em

FLOAT ou DOUBLE dá erro de arredondamento: eles guardam aproximação, e

0.1 + 0.2 não dá 0.3. A coluna date volta como Date do JavaScript, com

fuso horário aplicado — as 00:00:00 do banco viram 22:00:00 do dia anterior no

toISOString(). Para ler data como texto, a consulta precisa trazer no formato,

não no objeto.

O banco recusa o que não cabe

Com sql_mode estrito — o que a aula 1 mediu em @@sql_mode — escrever 60

caracteres em VARCHAR(40) dá ER_DATA_TOO_LONG, e escrever NULL em coluna

NOT NULL dá ER_BAD_NULL_ERROR. O erro.code é o que o Node compara; a

mensagem muda entre versões e não serve para decidir nada.

CREATE DATABASE, DROP DATABASE e as permissões

CREATE DATABASE exige privilégio no servidor, e o usuário do material não tem

— ele só enxerga o banco de teste, como a aula 1 mostrou no SHOW GRANTS. Por

isso nenhum exemplo deste material cria banco: ele usa o que já existe, dentro de

DB_NAME. Isso não é limitação do exemplo, é a garantia de que nenhum deles toca

no banco de produção.

Exemplo

'use strict';

// Exemplo da aula 2 do dia 8: criar banco e tabela, e conferir o que o banco fez.
//
// `CREATE TABLE` e o desenho da tabela. O exemplo cria a tabela do dia inteiro,
// consulta o catalogo para ver o que o SERVIDOR entendeu (tipo, nulo, chave) e
// compara com o que foi pedido — e e essa comparacao que ensina: o tipo que o
// Node manda (`'Ana'`, `3`) e o tipo que o SQL guardou (`varchar(40)`, `int`).
//
// O exemplo e idempotente: `CREATE TABLE IF NOT EXISTS` + `TRUNCATE` no COMECO.
// E o mesmo exemplo roda duas vezes seguidas e da a mesma saida, que e o que o
// material exige de todo exemplo.

const mysql = require('mysql2/promise');

async function main() {
  const conexao = await mysql.createConnection({
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT),
    user: process.env.DB_USER,
    password: process.env.DB_PASS,
    database: process.env.DB_NAME,
  });

  try {
    // --- 1. o desenho da tabela ---
    //
    // O `IF NOT EXISTS` e o que torna o exemplo repetivel. Sem ele, a segunda
    // execucao quebra com `ER_TABLE_EXISTS_ERROR` — e um exemplo que funciona
    // uma vez e quebra na segunda esta ensinando errado.
    await conexao.query(`
      CREATE TABLE IF NOT EXISTS tb_aluno (
        id        INT AUTO_INCREMENT PRIMARY KEY,
        nm_aluno  VARCHAR(40)  NOT NULL,
        email     VARCHAR(80)  NULL,
        turma     INT          NOT NULL,
        dt_cadastro DATE       NOT NULL,
        vl_nota   DECIMAL(4,2) NULL
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    `);
    console.log('--- a tabela criada ---');

    // `TRUNCATE` no COMECO e nao no fim: a aula seguinte precisa achar a tabela
    // no estado que este exemplo deixou.
    await conexao.query('TRUNCATE TABLE tb_aluno');

    // --- 2. `SHOW CREATE TABLE`: o que o servidor entendeu ---
    //
    // Esta e a forma de conferir o desenho depois de criado. O que volta e o SQL
    // que recria a tabela — e e nele que aparecem as Normalidades que o pedido
    // fez: o `NOT NULL` virou atributo de coluna, o `AUTO_INCREMENT` virou
    // atributo de tabela, e o tipo `VARCHAR(40)` foi guardado.
    const [criada] = await conexao.query('SHOW CREATE TABLE tb_aluno');
    console.log('');
    console.log('SHOW CREATE TABLE tb_aluno:');
    console.log(criada[0]['Create Table']);

    // --- 3. `SHOW COLUMNS`: coluna por coluna ---
    //
    // `Field` e o nome, `Type` o tipo guardado, `Null` se aceita nulo, `Key` o
    // papel da coluna (PRI = chave primaria), `Default` o valor padrao e
    // `Extra` o que mais o servidor guarda (auto_increment aqui).
    const [colunas] = await conexao.query('SHOW COLUMNS FROM tb_aluno');
    console.log('');
    console.log('--- as colunas, como o servidor as guardou ---');
    console.log('  ' + ['campo', 'tipo', 'aceita nulo', 'chave', 'extra'].join(' | '));
    for (const c of colunas) {
      console.log('  ' + [
        c.Field,
        String(c.Type),
        c.Null === 'YES' ? 'sim' : 'nao',
        c.Key || '-',
        c.Extra || '-',
      ].join(' | '));
    }
    console.log('  (PRI = chave primaria; auto_increment = a coluna que o banco numera)');
    console.log('  (todo NOT NULL sem DEFAULT aparece como `null` no SHOW COLUMNS: o');
    console.log('   padrao de coluna NOT NULL e a ausencia de valor, e o MySQL mostra');
    console.log('   `null` — o que nao e o mesmo que "aceita nulo", que e a coluna `email`)');

    // --- 4. a diferenca entre o que foi pedido e o que foi guardado ---
    //
    // O Node manda `'Ana'` e `3`, e o que chega no banco sao `varchar(40)` e
    // `int`. O tipo da coluna e um limite de tamanho, e o que entra dentro dele
    // e texto. E por isso que `VARCHAR(40)` de nome e `VARCHAR(255)` de email
    // nao sao a mesma coisa, mesmo sendo os dois "texto".
    const [grava] = await conexao.query(
      'INSERT INTO tb_aluno (nm_aluno, email, turma, dt_cadastro, vl_nota) VALUES (?, ?, ?, ?, ?)',
      ['Ana', '[email protected]', 3, '2026-05-04', 8.5]
    );
    console.log('');
    console.log('--- o que o Node manda contra o que o banco guarda ---');
    console.log("o Node mandou 'Ana' (texto), 3 (numero) e '2026-05-04' (texto)");
    console.log('o banco guardou como varchar(40), int e date: linhas gravadas =', grava.affectedRows);
    const [lida] = await conexao.query('SELECT nm_aluno, turma, dt_cadastro, vl_nota FROM tb_aluno');
    console.log('lendo de volta:', JSON.stringify(lida[0]));
    console.log('a data voltou como Date do JavaScript:', lida[0].dt_cadastro instanceof Date);
    console.log('e o vl_nota voltou como string, porque DECIMAL e exato:', typeof lida[0].vl_nota, JSON.stringify(lida[0].vl_nota));
    console.log('DECIMAL guarda exato; FLOAT e DOUBLE sao aproximados e nao servem para dinheiro.');

    // --- 5. o que o banco faz quando o dado nao cabe ---
    //
    // `VARCHAR(40)` com 50 caracteres: em modo estrito o banco RECUSA, e o
    // `sql_mode` visto na aula 1 e o que decide isso. O `ER_DATA_TOO_LONG` e o
    // erro que o Node tem de tratar — e ele e a prova de que o limite da coluna
    // vale.
    console.log('');
    console.log('--- limite de coluna, respeitado pelo banco ---');
    try {
      await conexao.query(
        'INSERT INTO tb_aluno (nm_aluno, turma, dt_cadastro) VALUES (?, ?, ?)',
        ['x'.repeat(60), 3, '2026-05-04']
      );
      console.log('  o banco ACEITOU 60 caracteres em VARCHAR(40) — o modo nao esta estrito');
    } catch (erro) {
      console.error(erro.code + ': ' + erro.message);
      console.log('  erro esperado:', erro.code, '- 60 caracteres em VARCHAR(40) nao cabem');
    }

    // --- 6. `NOT NULL` e o valor que nao pode faltar ---
    console.log('');
    console.log('--- NOT NULL ---');
    try {
      await conexao.query(
        'INSERT INTO tb_aluno (nm_aluno, turma, dt_cadastro) VALUES (?, ?, ?)',
        ['Sem Turma', null, '2026-05-04']
      );
      console.log('  o banco aceitou turma nula — a coluna nao tem NOT NULL');
    } catch (erro) {
      console.error(erro.code + ': ' + erro.message);
      console.log('  erro esperado:', erro.code, '- a coluna turma e NOT NULL');
    }

    // --- 7. `DROP TABLE` e por que o exemplo NAO usa ---
    //
    // `DROP TABLE` apaga a tabela e tudo que esta dentro. O material nao usa
    // `DROP` em nenhum exemplo, e o motivo e repetibilidade: o exemplo precisa
    // rodar duas vezes seguidas e dar a mesma saida, e `DROP` + `CREATE` faria a
    // segunda rodada perder o que a primeira gravou. `CREATE TABLE IF NOT EXISTS`
    // resolve o mesmo problema sem destruir estrutura.
    console.log('');
    console.log('--- por que nao ha DROP TABLE neste exemplo ---');
    console.log('DROP TABLE apaga tudo que esta dentro, e o exemplo precisa ser');
    console.log('repetivel: rodar duas vezes tem que dar a mesma saida.');
    console.log('IF NOT EXISTS + TRUNCATE resolvem sem destruir estrutura.');

    // --- 8. os tipos, e o que cada um guarda ---
    console.log('');
    console.log('--- os tipos de dado, e o que cada um guarda ---');
    console.log('CHAR(n)    | texto de tamanho fixo, preenchido com espaco');
    console.log('VARCHAR(n) | texto de ate n caracteres');
    console.log('TEXT       | texto longo, sem limite pratico');
    console.log('INT        | numero inteiro, de -2 bilhoes a 2 bilhoes');
    console.log('BIGINT     | inteiro maior');
    console.log('DECIMAL(p,s)| numero exato com p casas e s decimais — dinheiro');
    console.log('FLOAT/DOUBLE| aproximacao, para medida e nao para dinheiro');
    console.log('DATE       | so a data');
    console.log('DATETIME   | data e hora');
    console.log('BOOLEAN    | TRUE/FALSE, guardado como TINYINT(1)');
    console.log('JSON       | documento JSON, com validacao do banco');
    console.log('');
    console.log('a regra do material: nome de tabela e coluna em snake_case com prefixo');
    console.log('`tb_` (tb_aluno) e `dt_`/`vl_` para data e valor (dt_cadastro, vl_nota).');
  } finally {
    await conexao.end();
    console.log('');
    console.log('conexao encerrada com end().');
  }
}

main().catch((erro) => {
  console.error('falhou:', erro.code || erro.name, '-', erro.message);
  process.exit(1);
});

Saída real

--- a tabela criada ---

SHOW CREATE TABLE tb_aluno:
CREATE TABLE `tb_aluno` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `nm_aluno` varchar(40) NOT NULL,
  `email` varchar(80) DEFAULT NULL,
  `turma` int(11) NOT NULL,
  `dt_cadastro` date NOT NULL,
  `vl_nota` decimal(4,2) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

--- as colunas, como o servidor as guardou ---
  campo | tipo | aceita nulo | chave | extra
  id | int(11) | nao | PRI | auto_increment
  nm_aluno | varchar(40) | nao | - | -
  email | varchar(80) | sim | - | -
  turma | int(11) | nao | - | -
  dt_cadastro | date | nao | - | -
  vl_nota | decimal(4,2) | sim | - | -
  (PRI = chave primaria; auto_increment = a coluna que o banco numera)
  (todo NOT NULL sem DEFAULT aparece como `null` no SHOW COLUMNS: o
   padrao de coluna NOT NULL e a ausencia de valor, e o MySQL mostra
   `null` — o que nao e o mesmo que "aceita nulo", que e a coluna `email`)

--- o que o Node manda contra o que o banco guarda ---
o Node mandou 'Ana' (texto), 3 (numero) e '2026-05-04' (texto)
o banco guardou como varchar(40), int e date: linhas gravadas = 1
lendo de volta: {"nm_aluno":"Ana","turma":3,"dt_cadastro":"2026-05-03T22:00:00.000Z","vl_nota":"8.50"}
a data voltou como Date do JavaScript: true
e o vl_nota voltou como string, porque DECIMAL e exato: string "8.50"
DECIMAL guarda exato; FLOAT e DOUBLE sao aproximados e nao servem para dinheiro.

--- limite de coluna, respeitado pelo banco ---
  erro esperado: ER_DATA_TOO_LONG - 60 caracteres em VARCHAR(40) nao cabem

--- NOT NULL ---
  erro esperado: ER_BAD_NULL_ERROR - a coluna turma e NOT NULL

--- por que nao ha DROP TABLE neste exemplo ---
DROP TABLE apaga tudo que esta dentro, e o exemplo precisa ser
repetivel: rodar duas vezes tem que dar a mesma saida.
IF NOT EXISTS + TRUNCATE resolvem sem destruir estrutura.

--- os tipos de dado, e o que cada um guarda ---
CHAR(n)    | texto de tamanho fixo, preenchido com espaco
VARCHAR(n) | texto de ate n caracteres
TEXT       | texto longo, sem limite pratico
INT        | numero inteiro, de -2 bilhoes a 2 bilhoes
BIGINT     | inteiro maior
DECIMAL(p,s)| numero exato com p casas e s decimais — dinheiro
FLOAT/DOUBLE| aproximacao, para medida e nao para dinheiro
DATE       | so a data
DATETIME   | data e hora
BOOLEAN    | TRUE/FALSE, guardado como TINYINT(1)
JSON       | documento JSON, com validacao do banco

a regra do material: nome de tabela e coluna em snake_case com prefixo
`tb_` (tb_aluno) e `dt_`/`vl_` para data e valor (dt_cadastro, vl_nota).

conexao encerrada com end().