# PLATAFORMA — Modelagem de Dados (panorama do banco)

> Fonte primária: banco `prefeituras_v5` no container `v6_6f22e1bd-postgres-1`
> (PostgreSQL 10 + PostGIS). Complementado por `v6/database/migrations/`.
> Comandos de leitura: apenas `SELECT` / `\d` (nenhuma migration foi executada).

---

## 1. Números do banco (verificados)

| Métrica | v6 (implantado) | v5 (restaurado) |
|---|---|---|
| Tabelas base (`BASE TABLE`) | **138** | 130 |
| Views SQL (`information_schema.views`) | **75** | — |
| Migrations em `v6/database/migrations/` | **358** | 349 |
| Migrations que criam/alteram views | **126** | — |
| Linhas em `modulos` | **58** (56 na migration + 2 por seed/migration posterior) | tabela inexistente na v5 |
| Linhas em `resources` | **79** | — |
| Linhas em `menu_orders` | **45** | — |
| Linhas em `layouts` | **41** | — |

> A v5 **não tem** a tabela `modulos`: ela foi criada na v6 pela migration
> `v6/database/migrations/2024_11_01_150206_create_modulos_table.php`. A v5
> gerencia o menu lateral por `menu_orders` sem a abstração de módulo.

---

## 2. Convenção de nomenclatura

### 2.1 Tabelas

- **Plural `snake_case`** na maioria absoluta: `contents`, `archives`,
  `biddings`, `contracts`, `ombudsman_requests`, `service_letters`,
  `dashboard_widget_instances`, `menu_orders`, `informations_requests`.
- **Exceções documentadas** (singular ou nome próprio):

| Tabela | Motivo |
|---|---|
| `servidor` | nome único, sem plural (servidores públicos) |
| `viap` | sigla (Verba Indenizatória de Apoio Parlamentar) |
| `councilmen` | plural irregular em inglês |
| `activity_log` | nome do pacote `spatie/laravel-activitylog` |
| `addresses_type`, `emails_type`, `phones_type` | plural no início, singular no fim |
| `archives_biddings_legacy` | nomenclatura composta |
| `throttle`, `activations`, `password_resets`, `persistences` | tabelas de sessão/Sentinel (padrão do pacote) |

### 2.2 Tabelas pivot

Padrão `*_has_*` (4 tabelas) e `*_rel` (1 tabela):

| Pivot | Ligação |
|---|---|
| `biddings_has_archives` | `biddings` ↔ `archives` |
| `diario_has_areas` | `diarios` ↔ `diario_areas` |
| `emendas_has_archives` | `emendas_parlamentares` ↔ `archives` |
| `viap_has_archives` | `viap` ↔ `archives` |
| `service_letters_subjects_rel` | `service_letters` ↔ `service_letter_subjects` |
| `option_category_has_options` | `option_categories` ↔ `options` |
| `role_users` | `roles` ↔ `users` (padrão Sentinel, snake simples) |

### 2.3 Views SQL

As views **não** seguem um prefixo uniforme. Existem dois grupos:

- **Nome descritivo igual ao slug do conteúdo**: `noticias`, `publicacoes`,
  `legislacao`, `transparencia_fiscal`, `a_cidade`, `a_instituicao`,
  `a_orgaos`, `secretarias`, `enquetes`, `agenda_de_eventos`, `banners`,
  `super_banners`, `glossario`, `perguntas_frequentes`, `telefones_uteis`,
  `acesso_rapido`, `anuncios`, `fornecedores`, `licitacao`, `contratos`,
  `propostas`, `proponentes`, `proponentes_pf`, `proponentes_pj`,
  `diario_oficial`, `sessoes`, `atas_e_pontos`, `pauta_do_dia`,
  `comite_de_investimentos`, `requerimentos`, `req_diarias`, `unidade_gestora`,
  `unidades_de_atendimento`, `tv_camara`, `formulario`, `respostas`,
  `redes_social`, `menus_site`, `paginas_estaticas`, `servidores`,
  `secretarias_paginas`, `secretarias_categorias`, `pauta_do_dia`.
- **Prefixo `listagem_` / `listar_` / `view_`**: `listagem_servicos_carta_de_servicos`,
  `listagem_assuntos_carta_de_servicos`, `listagem_clientes`,
  `listagem_questionarios`, `listagem_questionarios_preenchidos`,
  `listar_todos_usuarios`, `listar_usuarios`, `listar_usuarios_inativos`,
  `view_emendas_parlamentares`, `view_viap`.

**Contradição com a convenção declarada:** o enunciado do dossiê fala em
`v_*`; o banco real **não tem nenhuma view com prefixo `v_`**. O prefixo
`view_` aparece em apenas 2 das 75 views (`view_emendas_parlamentares`,
`view_viap`). Registrar a divergência: a nomenclatura real é
**descritiva, sem prefixo padronizado**.

### 2.4 Colunas

- snake_case, sem aspas, em minúsculas. Exceções: `contents."order"` (palavra
  reservada → aspas), `contents.galery` (galeria, grafia herdada).
- Chaves primárias: `id` `integer` com `nextval('..._id_seq')` em todas as
  tabelas do application.
- Timestamps: `created_at` / `updated_at` como `timestamp(0) without time zone`.
- Soft delete: coluna `deleted_at` `timestamp(0) without time zone` (nullable)
  em 98 das 358 migrations.

---

## 3. Views SQL — o mecanismo central de leitura

### 3.1 Como funcionam

As views são criadas por migrations que executam `DB::statement("CREATE OR REPLACE VIEW …")`.
**126 das 358 migrations** (35%) criam ou alteram views. A data de criação é
a chave da atualização: o sistema usa `CREATE OR REPLACE VIEW`, então cada
meltdown reescreve a view inteira.

Padrão de nome de migration: `YYYY_MM_DD_HHMMSS_create_view_to_<slug>.php`
e `YYYY_MM_DD_HHMMSS_update_view_<slug>_<motivo>.php`.

Exemplo real — `v6/database/migrations/2023_04_10_144637_create_view_to_a_cidade.php`:

```php
public function up()
{
    DB::statement("
        CREATE OR REPLACE VIEW a_cidade AS
        SELECT row_number() OVER (ORDER BY c.created_at DESC) AS value,
            c.title AS titulo,
            c.id as id
        FROM contents c
            JOIN category_contents cc ON cc.id = c.category_contents_id
        WHERE c.deleted_at IS NULL AND cc.slug = 'a-cidade'
        ORDER BY c.created_at DESC;
    ");
}
```

Exemplo real — `v6/database/migrations/2023_03_15_115654_create_view_to_noticias.php`:

```sql
CREATE OR REPLACE VIEW noticias
as select
    row_number() OVER (ORDER BY c.highlights is true desc) as value,
    c.title as titulo,
    CASE
       WHEN c.status is true  THEN 'Publicado'
       ELSE 'Rascunho'
    END AS status_name_noticias,
    to_char(c.publication_date::timestamp with time zone, 'DD/MM/YYYY'::text) AS data_publicacao,
    cc."name" as categoria,
    c.id as id,
    c.highlights as destaque,
    c.galery as galeria
from contents c
    JOIN category_contents cc ON cc.id = c.category_contents_id
where
    c.deleted_at is null
and
    cc.category_contents_id = 1
order by c.highlights is true desc ;
```

Exemplo real — `v6/database/migrations/2023_04_11_094706_create_view_to_transparencia_fiscal.php`:

```sql
CREATE OR REPLACE VIEW transparencia_fiscal AS
SELECT row_number() OVER (ORDER BY (c.status IS TRUE) DESC, c.created_at DESC) AS value,
    c.title AS titulo,
    c.id as id,
CASE
    WHEN c.status IS TRUE THEN 'Ativado'::text
    ELSE 'Desativado'::text
END AS status_name,
    c.status
FROM contents c
    JOIN category_contents cc ON cc.id = c.category_contents_id
WHERE c.deleted_at IS NULL AND cc.slug = 'transparencia-fiscal'
ORDER BY (c.status IS TRUE) DESC, c.created_at DESC;
```

Exemplo real — `v6/database/migrations/2026_01_20_160719_create_service_letters_table.php`
e a view `listagem_servicos_carta_de_servicos` (estado atual no banco):

```sql
SELECT row_number() OVER (ORDER BY sl.created_at DESC) AS value,
    sl.nome,
    c.title AS secretaria,
    ( SELECT array_to_string(array_agg(sls.nome), ', '::text) AS array_to_string
           FROM (service_letter_subjects sls
             JOIN service_letters_subjects_rel slsr ON (((slsr.subject_id = sls.id) AND (slsr.service_letter_id = sl.id))))
          GROUP BY slsr.service_letter_id) AS assuntos,
    sl.menu_acesso_rapido AS menu,
    CASE WHEN sl.ativo THEN 'ATIVO'::text ELSE 'INATIVO'::text END AS status,
    CASE WHEN sl.destaque THEN 'SIM'::text ELSE 'NÃO'::text END AS destaque,
    sl.created_at AS data_criacao,
    sl.updated_at AS data_atualizacao,
    sl.id
FROM (service_letters sl
  LEFT JOIN contents c ON ((c.id = sl.secretaria_id)))
ORDER BY sl.ordem, sl.nome;
```

### 3.2 O que as views substituem

| Padrão | SQL direto equivalente | Ganho |
|---|---|---|
| `contents` + `category_contents` por slug | JOIN + filtro `cc.slug = 'x'` | 1 linha na view |
| Enumeração de status (`Publicado`/`Rascunho`) | `CASE WHEN status …` no PHP | evita lógica de apresentação no controller |
| `row_number() OVER (ORDER BY …)` | subquery de paginação | a view já entrega índice |
| Formatação de data `DD/MM/YYYY` | `to_char(…)` no controller | formatação no banco |
| Agregação de assuntos relacionados | subselect com `array_agg` | evita N+1 no PHP |

**Ponto central:** os controllers do CMS consultam as views como se fossem
tabelas (`DB::table('noticias')->get()` — `v6/app/Http/Controllers/Prefecture/DatatableController.php:21`).
Isso significa que a **view é o contrato de leitura** do CMS. Na v7, a
alternativa moderna é manter a view (o banco já a tem) mas expor via
**API Resources** e **Query Objects**, para que a camada de apresentação não
dependa do formato exato da view.

### 3.3 Views com `row_number()` e suas consequências

26 views incluem `row_number() OVER (…) AS value`. Esse `value` é usado como
**índice de ordenação** manual no `orderLayout()` do `LayoutController`
(`v6/app/Http/Controllers/Prefecture/LayoutController.php:120-135`) e como
coluna de exibição em listagens do CMS.

**Risco de migração:** se a view for recriada com um `ORDER BY` diferente, a
ordem manual de widgets/banners do CMS muda silenciosamente.

---

## 4. Estratégia de migrations

| Aspecto | Realidade v6 |
|---|---|
| Formato de arquivo | `YYYY_MM_DD_HHMMSS_nome.php`, classe `CreateModulosTable`, `up()`/`down()` |
| Total | 358 arquivos (v5: 349) |
| SQL bruto | `DB::statement()` em **382** ocorrências, todas em `v6/database/migrations/` (zero em `app/`) |
| `doctrine/dbal` | **Presente** em `v6/composer.lock` (versão `2.13.9`) como dependência de `yajra/laravel-datatables`; **nenhuma migration do projeto importa `Doctrine\DBAL`** (grep: 0 ocorrências em `v6/database/migrations/`) |
| Alteração de coluna | Feita com `DB::statement('ALTER TABLE …')` cru, não com `->change()` |
| Renomear coluna | idem (ex.: `v6/database/migrations/2019_09_10_161305_alter_link_to_links_table.php`) |
| Rollback | `down()` presente em quase todas; mas as views usam `DROP VIEW if exists public.<nome>` |
| Idempotência | `CREATE OR REPLACE VIEW` garante reexecução; `Schema::create` **não** |

**Conclusão importante para a v7:** a v6 **não usa** `doctrine/dbal` para
alterações despite the package being in the lock. Todas as alterações de
coluna são SQL crudo via `DB::statement()`. Isso significa que a estratégia de
migrations da v6 é **frágil** (sem dry-run, sem diff automático) e precisa ser
substituída na v7 por migrations declarativas com `->change()` + `doctrine/dbal`
(ou pelo schema builder moderno do Laravel 11).

### 4.1 Soft deletes, casts e mutators

| Conceito | Contagem / evidência |
|---|---|
| Migrations com `softDeletes()` | **98** de 358 |
| Models com `use SoftDeletes` | **86** de 131 models |
| Models com `protected $casts` | **17** |
| Models com mutator `set<Attribute>()` | **3** |
| Models com `protected $dates` | presentes (ex.: `Content::$dates = ['publication_date']`) |

Exemplo real — `v6/app/Http/Model/Prefecture/Content.php:12-56`:

```php
namespace App\Prefecture;

use App\Http\Presenters\ContentPresenter;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
use Laracasts\Presenter\PresentableTrait;

class Content extends Model
{
    use SoftDeletes;
    use PresentableTrait;

    protected $table = 'contents';
    protected $fillable = [ /* 25 campos */ ];

    protected $dates = [
        'publication_date',
    ];

    protected $primaryKey = 'id';

    protected $casts = [
        'publication_date' => 'datetime',
    ];

    protected $presenter = ContentPresenter::class;
}
```

> Note: o model `Content` **não** declara `protected $table` coerente com
> `category_contents_id` — mas o nome da tabela é plural (`contents`), enquanto
> os models `App\Prefecture\*` (41 arquivos) e `App\Http\Model\Prefecture\*`
> (40 arquivos) usam o mesmo diretório.

---
