Stored Procedures
Stored Procedures (DML & DCL)
Introdução
As Stored Procedures são blocos de código SQL armazenados no banco de dados que encapsulam lógicas complexas e promovem segurança, reutilização e desempenho. Segundo Elmasri e Navathe, elas são fundamentais para padronizar o acesso aos dados, aplicar regras de negócio de forma centralizada e reduzir a duplicação de comandos SQL.
Este documento apresenta as principais procedures e functions do sistema Munchkin, organizadas por categoria funcional. Cada bloco de código está documentado com comentários explicativos, e as operações são protegidas por variáveis de sessão que garantem a integridade das transações.
1. Criar partida segura
Este stored procedure serve para iniciar uma nova partida distribuindo automaticamente 14 cartas (7 porta + 7 tesouro) para a mão do jogador.
Comandos
-- Function segura para iniciar uma partida.
-- Distribui exatamente 14 cartas (7 porta + 7 tesouro) para a mão do jogador.
-- Retorna: (id_partida, status da operação)
CREATE OR REPLACE FUNCTION iniciar_partida_segura(p_id_jogador INTEGER)
RETURNS TABLE(p_id_partida INTEGER, p_status VARCHAR(50))
LANGUAGE plpgsql AS $$
DECLARE
partida_existente INTEGER;
carta_record RECORD;
contador_cartas INTEGER := 0;
cartas_porta INTEGER := 0;
cartas_tesouro INTEGER := 0;
nova_partida_id INTEGER;
resultado_status VARCHAR(50);
BEGIN
-- Verifica se o jogador existe
IF NOT EXISTS (SELECT 1 FROM jogador WHERE id_jogador = p_id_jogador) THEN
RAISE EXCEPTION 'Jogador com ID % não encontrado!', p_id_jogador;
END IF;
-- Verifica se já há partida em andamento
SELECT id_partida INTO partida_existente
FROM partida
WHERE id_jogador = p_id_jogador AND estado_partida = 'em andamento';
IF partida_existente IS NOT NULL THEN
nova_partida_id := partida_existente;
resultado_status := 'PARTIDA_EXISTENTE';
p_id_partida := nova_partida_id;
p_status := resultado_status;
RETURN NEXT;
RETURN;
END IF;
-- Verifica disponibilidade de cartas
SELECT COUNT(*) INTO cartas_porta
FROM carta WHERE tipo_carta = 'porta' AND disponivel_para_virar = TRUE;
SELECT COUNT(*) INTO cartas_tesouro
FROM carta WHERE tipo_carta = 'tesouro' AND disponivel_para_virar = TRUE;
IF cartas_porta < 7 OR cartas_tesouro < 7 THEN
RAISE EXCEPTION 'Cartas insuficientes! Disponível: % porta, % tesouro (mínimo: 7 de cada)',
cartas_porta, cartas_tesouro;
END IF;
-- Autoriza criação controlada
PERFORM set_config('app.criacao_partida_autorizada', 'true', true);
-- Cria nova partida
INSERT INTO partida (id_jogador, data_inicio, estado_partida, vida_restantes)
VALUES (p_id_jogador, NOW(), 'em andamento', 3)
RETURNING id_partida INTO nova_partida_id;
-- Distribui 7 cartas do tipo porta
FOR carta_record IN (
SELECT id_carta FROM carta
WHERE tipo_carta = 'porta' AND disponivel_para_virar = TRUE
ORDER BY RANDOM() LIMIT 7
) LOOP
INSERT INTO carta_partida (id_partida, id_carta, zona)
VALUES (nova_partida_id, carta_record.id_carta, 'mao');
contador_cartas := contador_cartas + 1;
END LOOP;
-- Distribui 7 cartas do tipo tesouro
FOR carta_record IN (
SELECT id_carta FROM carta
WHERE tipo_carta = 'tesouro' AND disponivel_para_virar = TRUE
ORDER BY RANDOM() LIMIT 7
) LOOP
INSERT INTO carta_partida (id_partida, id_carta, zona)
VALUES (nova_partida_id, carta_record.id_carta, 'mao');
contador_cartas := contador_cartas + 1;
END LOOP;
-- Verificação de segurança
IF contador_cartas != 14 THEN
RAISE EXCEPTION 'Erro crítico na distribuição: esperado 14 cartas, recebido %', contador_cartas;
END IF;
-- Limpa variável de controle
PERFORM set_config('app.criacao_partida_autorizada', '', true);
-- Retorna resultado
p_id_partida := nova_partida_id;
p_status := 'NOVA_PARTIDA_CRIADA';
RETURN NEXT;
END;
$$;
COMMENT ON FUNCTION iniciar_partida_segura(INTEGER) IS
'Inicia partida com 14 cartas (7 porta + 7 tesouro), verificando integridade e protegendo via variável de sessão.';
2. Excluir jogador completo
Este stored procedure serve para remover um jogador e todos os seus dados vinculados (partidas, cartas, combates) do sistema.
Comandos
-- Procedure para exclusão total de um jogador e seus dados vinculados.
CREATE OR REPLACE PROCEDURE excluir_jogador_completo(p_id_jogador INTEGER)
LANGUAGE plpgsql AS $$
DECLARE
partida_record RECORD;
BEGIN
-- Verifica existência
IF NOT EXISTS (SELECT 1 FROM jogador WHERE id_jogador = p_id_jogador) THEN
RAISE EXCEPTION 'Jogador com ID % não encontrado!', p_id_jogador;
END IF;
-- Autoriza exclusão
PERFORM set_config('app.exclusao_autorizada', 'true', true);
FOR partida_record IN SELECT id_partida FROM partida WHERE id_jogador = p_id_jogador LOOP
DELETE FROM uso_poder_venda WHERE id_partida = partida_record.id_partida;
DELETE FROM combate WHERE id_partida = partida_record.id_partida;
DELETE FROM carta_partida WHERE id_partida = partida_record.id_partida;
DELETE FROM partida WHERE id_partida = partida_record.id_partida;
END LOOP;
DELETE FROM jogador WHERE id_jogador = p_id_jogador;
PERFORM set_config('app.exclusao_autorizada', '', true);
END;
$$;
COMMENT ON PROCEDURE excluir_jogador_completo(INTEGER) IS
'Remove um jogador e todas as suas partidas, cartas e combates associados.';
3. Excluir partidas do jogador
Este stored procedure serve para remover apenas as partidas de um jogador específico, mantendo o cadastro do jogador no sistema.
Comandos
-- Procedure que remove apenas as partidas do jogador, preservando seu cadastro.
CREATE OR REPLACE PROCEDURE excluir_partidas_jogador(p_id_jogador INTEGER)
LANGUAGE plpgsql AS $$
DECLARE
partida_record RECORD;
BEGIN
IF NOT EXISTS (SELECT 1 FROM jogador WHERE id_jogador = p_id_jogador) THEN
RAISE EXCEPTION 'Jogador com ID % não encontrado!', p_id_jogador;
END IF;
PERFORM set_config('app.exclusao_autorizada', 'true', true);
FOR partida_record IN SELECT id_partida FROM partida WHERE id_jogador = p_id_jogador LOOP
DELETE FROM uso_poder_venda WHERE id_partida = partida_record.id_partida;
DELETE FROM combate WHERE id_partida = partida_record.id_partida;
DELETE FROM carta_partida WHERE id_partida = partida_record.id_partida;
DELETE FROM partida WHERE id_partida = partida_record.id_partida;
END LOOP;
PERFORM set_config('app.exclusao_autorizada', '', true);
END;
$$;
COMMENT ON PROCEDURE excluir_partidas_jogador(INTEGER) IS
'Remove com segurança todas as partidas de um jogador, sem apagar o jogador.';
4. Movimentar cartas entre zonas
Este stored procedure serve para mover uma carta de uma zona para outra (mão, equipado, descartado) de forma segura e controlada.
Comandos
-- Procedure segura para movimentar cartas entre zonas
CREATE OR REPLACE PROCEDURE mover_carta_zona_seguro(
p_id_partida INTEGER,
p_id_carta INTEGER,
p_nova_zona enum_zona
)
LANGUAGE plpgsql AS $$
DECLARE
existe_carta INTEGER;
BEGIN
-- Verificar se a carta pertence à partida fornecida
SELECT COUNT(*) INTO existe_carta
FROM carta_partida
WHERE id_partida = p_id_partida AND id_carta = p_id_carta;
IF existe_carta = 0 THEN
RAISE EXCEPTION 'A carta % não pertence à partida %!', p_id_carta, p_id_partida;
END IF;
-- Autorizar temporariamente a alteração da zona
PERFORM set_config('app.mudanca_zona_autorizada', 'true', true);
-- Alterar a zona com permissão
UPDATE carta_partida
SET zona = p_nova_zona
WHERE id_partida = p_id_partida AND id_carta = p_id_carta;
-- Limpar a autorização ao final
PERFORM set_config('app.mudanca_zona_autorizada', '', true);
RAISE NOTICE '✅ Carta % movida para a zona % com sucesso.', p_id_carta, p_nova_zona;
EXCEPTION
WHEN OTHERS THEN
-- Limpar a permissão em caso de erro
PERFORM set_config('app.mudanca_zona_autorizada', '', true);
RAISE EXCEPTION 'Erro ao mover carta %: %', p_id_carta, SQLERRM;
END;
$$;
COMMENT ON PROCEDURE mover_carta_zona_seguro(INTEGER, INTEGER, enum_zona) IS
'Move uma carta entre zonas dentro de uma partida de forma controlada e segura, validando autorização via variável de sessão.';
5. Equipar carta segura
Este stored procedure serve para equipar uma carta aplicando todas as regras do jogo (validação de slots, restrições de raça/classe, bônus automáticos).
Comandos
-- Procedure segura para equipar uma carta aplicando regras e poderes
CREATE OR REPLACE PROCEDURE equipar_carta_segura(
p_id_partida INTEGER,
p_id_carta INTEGER
)
LANGUAGE plpgsql AS $$
DECLARE
v_subtipo TEXT;
v_slot TEXT;
v_bonus_combate INTEGER;
v_limite_mao INTEGER;
v_ocupacao_dupla BOOLEAN;
v_tipo_alvo TEXT;
v_valor_alvo TEXT;
v_permitido BOOLEAN;
v_possui_alvo BOOLEAN;
v_conflito INTEGER;
restr RECORD;
BEGIN
-- Obter subtipo da carta
SELECT subtipo INTO v_subtipo
FROM carta
WHERE id_carta = p_id_carta;
-- Impedir equipar monstro
IF v_subtipo = 'monstro' THEN
RAISE EXCEPTION '❌ Você não pode equipar cartas do tipo MONSTRO.';
END IF;
-- Impedir múltiplas raças
IF v_subtipo = 'raca' THEN
IF EXISTS (
SELECT 1
FROM carta_partida cp
JOIN carta c ON c.id_carta = cp.id_carta
WHERE cp.id_partida = p_id_partida
AND cp.zona = 'equipado'
AND c.subtipo = 'raca'
) THEN
RAISE EXCEPTION '❌ Você já tem uma raça equipada. Desequipe-a antes de equipar outra.';
END IF;
END IF;
-- Impedir múltiplas classes
IF v_subtipo = 'classe' THEN
IF EXISTS (
SELECT 1
FROM carta_partida cp
JOIN carta c ON c.id_carta = cp.id_carta
WHERE cp.id_partida = p_id_partida
AND cp.zona = 'equipado'
AND c.subtipo = 'classe'
) THEN
RAISE EXCEPTION '❌ Você já tem uma classe equipada. Desequipe-a antes de equipar outra.';
END IF;
END IF;
-- Caso a carta seja uma raça com poder de limite de mão
IF v_subtipo = 'raca' THEN
SELECT pl.limite_cartas_mao INTO v_limite_mao
FROM poder_raca pr
JOIN poder_limite_de_mao pl ON pr.id_poder_raca = pl.id_poder_raca
WHERE pr.id_carta = p_id_carta;
-- Se existir poder, atualiza o limite
IF v_limite_mao IS NOT NULL THEN
UPDATE partida
SET limite_mao_atual = v_limite_mao
WHERE id_partida = p_id_partida;
RAISE NOTICE '🧬 Limite de cartas na mão atualizado para % devido ao poder da raça.', v_limite_mao;
END IF;
END IF;
-- Caso item: verificar restrições e aplicar bônus
IF v_subtipo = 'item' THEN
-- Aplicar verificações de restrição
FOR restr IN
SELECT tipo_alvo, valor_alvo, permitido
FROM restricao_item
WHERE id_carta_item = p_id_carta
LOOP
v_tipo_alvo := restr.tipo_alvo;
v_valor_alvo := restr.valor_alvo;
v_permitido := restr.permitido;
IF v_tipo_alvo = 'classe' THEN
SELECT EXISTS (
SELECT 1
FROM carta_partida cp
JOIN carta_classe cc ON cc.id_carta = cp.id_carta
WHERE cp.id_partida = p_id_partida AND cp.zona = 'equipado' AND cc.nome_classe = v_valor_alvo
) INTO v_possui_alvo;
ELSIF v_tipo_alvo = 'raca' THEN
SELECT EXISTS (
SELECT 1
FROM carta_partida cp
JOIN carta_raca cr ON cr.id_carta = cp.id_carta
WHERE cp.id_partida = p_id_partida AND cp.zona = 'equipado' AND cr.nome_raca = v_valor_alvo
) INTO v_possui_alvo;
END IF;
IF v_permitido AND NOT v_possui_alvo THEN
RAISE EXCEPTION '❌ Este item só pode ser usado por %: %, e você não está com isso equipado.', v_tipo_alvo, v_valor_alvo;
ELSIF NOT v_permitido AND v_possui_alvo THEN
RAISE EXCEPTION '❌ Este item não pode ser usado por %: %, e você está com isso equipado.', v_tipo_alvo, v_valor_alvo;
END IF;
END LOOP;
-- Obter dados do item
SELECT slot, bonus_combate, ocupacao_dupla INTO v_slot, v_bonus_combate, v_ocupacao_dupla
FROM carta_item
WHERE id_carta = p_id_carta;
-- Validação de slot ocupado
IF v_slot != 'nenhum' THEN
IF v_slot = '2_maos' THEN
-- Verifica se já tem qualquer item nas mãos
SELECT COUNT(*) INTO v_conflito
FROM carta_partida cp
JOIN carta_item ci ON ci.id_carta = cp.id_carta
WHERE cp.id_partida = p_id_partida AND cp.zona = 'equipado'
AND ci.slot IN ('1_mao', '2_maos');
IF v_conflito > 0 THEN
RAISE EXCEPTION '❌ Você já está usando as mãos. Remova os itens antes de equipar um que ocupa 2 mãos.';
END IF;
ELSIF v_slot = '1_mao' THEN
-- Verifica se já tem um item de 2_maos
SELECT COUNT(*) INTO v_conflito
FROM carta_partida cp
JOIN carta_item ci ON ci.id_carta = cp.id_carta
WHERE cp.id_partida = p_id_partida AND cp.zona = 'equipado'
AND ci.slot = '2_maos';
IF v_conflito > 0 THEN
RAISE EXCEPTION '❌ Você já está usando um item que ocupa 2 mãos. Não pode equipar outro nas mãos.';
END IF;
ELSE
-- Verifica se já existe outro item no mesmo slot
SELECT COUNT(*) INTO v_conflito
FROM carta_partida cp
JOIN carta_item ci ON ci.id_carta = cp.id_carta
WHERE cp.id_partida = p_id_partida AND cp.zona = 'equipado'
AND ci.slot = v_slot;
IF v_conflito > 0 AND NOT v_ocupacao_dupla THEN
RAISE EXCEPTION '❌ Você já tem um item equipado no slot "%".', v_slot;
END IF;
END IF;
END IF;
-- Aplica bônus de combate
UPDATE partida
SET nivel = nivel + v_bonus_combate
WHERE id_partida = p_id_partida;
RAISE NOTICE '🪖 Item equipado no slot "%". Bônus de combate +% aplicado.', v_slot, v_bonus_combate;
END IF;
-- Mover para a zona "equipado" com segurança
CALL mover_carta_zona_seguro(p_id_partida, p_id_carta, 'equipado');
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION '❌ Erro ao equipar carta %: %', p_id_carta, SQLERRM;
END;
$$;
COMMENT ON PROCEDURE equipar_carta_segura(INTEGER, INTEGER) IS
'Equipa uma carta validando todas as regras do jogo (raça/classe única, restrições de item, slots) e aplicando efeitos automaticamente.';
6. Desequipar carta segura
Este stored procedure serve para desequipar uma carta removendo seus efeitos e gerenciando itens dependentes automaticamente.
Comandos
-- Procedure segura para desequipar uma carta removendo efeitos
CREATE OR REPLACE PROCEDURE desequipar_carta_segura(
p_id_partida INTEGER,
p_id_carta INTEGER
)
LANGUAGE plpgsql AS $$
DECLARE
v_subtipo TEXT;
v_nome TEXT;
v_bonus_combate INTEGER;
v_dependentes RECORD;
BEGIN
-- Verificar se a carta está realmente equipada
IF NOT EXISTS (
SELECT 1
FROM carta_partida
WHERE id_partida = p_id_partida AND id_carta = p_id_carta AND zona = 'equipado'
) THEN
RAISE EXCEPTION '❌ Esta carta não está equipada.';
END IF;
-- Obter subtipo e nome para mensagens
SELECT subtipo, nome INTO v_subtipo, v_nome
FROM carta
WHERE id_carta = p_id_carta;
-- Se for raça ou classe, verificar dependência de itens
IF v_subtipo IN ('raca', 'classe') THEN
FOR v_dependentes IN
SELECT ci.id_carta AS id_item, c.nome
FROM carta_partida cp
JOIN carta_item ci ON ci.id_carta = cp.id_carta
JOIN restricao_item ri ON ri.id_carta_item = ci.id_carta
JOIN carta c ON c.id_carta = ci.id_carta
WHERE cp.id_partida = p_id_partida
AND cp.zona = 'equipado'
AND ri.tipo_alvo = v_subtipo
AND ri.valor_alvo = v_nome
AND ri.permitido = true
LOOP
RAISE NOTICE '🧤 O item "%" foi automaticamente desequipado pois depende da sua % "%".', v_dependentes.nome, v_subtipo, v_nome;
-- Mover o item dependente para a mão
CALL mover_carta_zona_seguro(p_id_partida, v_dependentes.id_item, 'mao');
END LOOP;
END IF;
-- Se for item, remover bônus de combate
IF v_subtipo = 'item' THEN
SELECT bonus_combate INTO v_bonus_combate
FROM carta_item
WHERE id_carta = p_id_carta;
UPDATE partida
SET nivel = nivel - v_bonus_combate
WHERE id_partida = p_id_partida;
RAISE NOTICE '🪖 Item "%": bônus de combate -% removido.', v_nome, v_bonus_combate;
END IF;
-- Mover a carta para a mão
CALL mover_carta_zona_seguro(p_id_partida, p_id_carta, 'mao');
RAISE NOTICE '✅ Carta "%" foi movida de EQUIPADO para MÃO.', v_nome;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION '❌ Erro ao desequipar carta %: %', p_id_carta, SQLERRM;
END;
$$;
COMMENT ON PROCEDURE desequipar_carta_segura(INTEGER, INTEGER) IS
'Desequipa uma carta removendo seus efeitos e gerenciando dependências de itens que requerem raça/classe específica.';
Referência Bibliográfica
[1] ELMASRI, Ramez; NAVATHE, Shamkant B. Sistemas de banco de dados. 6. ed. São Paulo: Pearson Addison Wesley, 2011.
Versionamento
Versão | Data | Modificação | Autor(es) |
---|---|---|---|
0.1 | 06/07/2025 | Criação do Documento e Procedures | Mylena Mendonça |
1.0 | 06/07/2025 | Organização e adaptação final | Maria Clara Sena |
1.1 | 07/07/2025 | Ajustes na formatação geral | Breno Soares Fernandes |