Overview

The Observatorio del Congreso uses a Popolo-Graph unified schema stored in SQLite. It contains 12 tables organized into four functional groups:

GroupTablesPurpose
Core entitiesperson, organization, areaLegislators, parties, geographic divisions
Legislative activitymotion, vote_event, vote, countBills, voting sessions, individual votes, tallies
Structural relationsmembership, postParty affiliations, committee seats
Power networksrelacion_poder, actor_externo, evento_politicoLoyalty ties, external actors, political events

All tables use text-based primary keys with a prefix convention (e.g. P00001, VE_D00001). Foreign keys enforce referential integrity — 14 foreign keys total: 4 with ON DELETE CASCADE (child tables) and 10 with ON DELETE RESTRICT (lookup/parent tables). CHECK constraints validate enums and ranges at the database level, using the correct IS NULL OR field IN(...) pattern for nullable enums.

Entity-Relationship Diagram

                        +-------------+
                        |    area     |
                        +-------------+
                        | id (PK)     |
                        | nombre      |
                        | parent_id FK|----+
                        +-------------+    |
                               ^          |
                               |          |
+---------------+    +------------------+  |  +------------------+
| organization  |    |      post        |  |  | actor_externo    |
+---------------+    +------------------+  |  +------------------+
| id (PK)       |<---| org_id (FK)      |  |  | id (PK)          |
| nombre        |    | area_id (FK)     |--+  | nombre           |
| abbr          |    +------------------+     | tipo             |
| clasificacion |                             | area_id (FK)     |--+
+------+--------+                             +------------------+  |
       |            +----------------+                              |
       |            |   membership   |                              |
       |            +----------------+                              |
       |            | id (PK)        |                              |
       +---org_id-->| person_id (FK) |-----+                        |
       |            | rol            |     |                        |
       |            | on_behalf_of FK|-----+                        |
       |            +----------------+     |                        |
       |                                   v                        |
       |                            +-------------+                 |
       |                            |    person   |                 |
       |                            +-------------+                 |
       |                            | id (PK)     |                 |
       |                            | nombre      |                 |
       |                            | genero      |                 |
       |                            +-------------+                 |
       |                                   |                        |
       |                                   v                        |
       |       +----------------+   +-------------+                 |
       |       |    vote_event  |   |    vote     |                 |
       |       +----------------+   +-------------+                 |
       +--org_id| id (PK)       |<--| vote_event  |                 |
               | motion_id (FK)|   | voter_id FK |                 |
               +-------+--------+   +-------------+                 |
                       |                                            |
                       v                                            |
                +-------------+       +------------------+          |
                |   motion    |       |   count          |          |
                +-------------+       +------------------+          |
                | id (PK)     |       | id (PK)          |          |
                | texto       |       | vote_event_id FK |----------+
                | clasificacion       | option           |          |
                +-------------+       | group_id (FK) ---+----------+
                                      +------------------+

        +-------------------+     +-------------------+
        | relacion_poder    |     | evento_politico   |
        +-------------------+     +-------------------+
        | id (PK)           |     | id (PK)           |
        | source_type       |     | fecha             |
        | source_id         |     | tipo              |
        | target_type       |     | descripcion       |
        | target_id         |     | motion_id (FK) ---+--> motion
        | tipo              |     +-------------------+
        | peso (1-5)        |
        +-------------------+
  source/target: person | organization | actor_externo

ID Convention

All primary keys use a text prefix to identify the entity type and, where applicable, the chamber of origin. Numeric portions are zero-padded to 5 digits.

EntityPrefixExamplePadding
Vote Event (Diputados)VE_DVE_D000015 digits
Vote Event (Senado)VE_SVE_S000015 digits
Vote (Diputados)V_DV_D000015 digits
Vote (Senado)V_SV_S000015 digits
Motion (Diputados)Y_DY_D000015 digits
Motion (Senado)Y_SY_S000015 digits
Membership (Diputados)M_DM_D000015 digits
Membership (Senado)M_SM_S000015 digits
PersonPP000015 digits
CountCC000015 digits

:::note Chamber-specific prefixes (_D for Diputados, _S for Senado) are used for entities that differ between chambers. Shared entities like person and count use a single prefix. :::

Key Organizations

These are the core political organizations stored in the organization table:

IDAbbreviationNameClassification
O08DiputadosCámara de Diputadosinstitucion
O09SenadoSenado de la Repúblicainstitucion
O10SHHSigamos Haciendo Historiacoalicion
O01MORENAMovimiento de Regeneración Nacionalpartido
O02PTPartido del Trabajopartido
O03PVEMPartido Verde Ecologista de Méxicopartido
O04PANPartido Acción Nacionalpartido
O05PRIPartido Revolucionario Institucionalpartido
O06MCMovimiento Ciudadanopartido
O07PRDPartido de la Revolución Democráticapartido
O11Ind.Independientespartido
O12CONVConvergenciapartido
O13NANueva Alianzapartido
O14ALTAlternativa Socialdemócratapartido
O15PESPartido Encuentro Socialpartido
O16SPSin Partidopartido

:::note O08 (Diputados) and O09 (Senado) are classified as institucion, not gobierno. O10 (SHH) is the ruling coalition. Historical parties (O12–O15) may have dissolution dates set. :::

Table-by-Table Reference

area — Geographic Divisions

Stores geographic divisions used for electoral districts and states. Areas can be nested via parent_id to model the hierarchy: district within state.

ColumnTypeNullableDescription
idTEXTNO (PK)Primary key, prefix A01, A02
nombreTEXTNOName of the area
clasificacionTEXTNOClassification: estado, distrito, circunscripcion
parent_idTEXTYESForeign key to area(id), ON DELETE RESTRICT. District points to its parent state
geometryTEXTYESOptional GeoJSON geometry
clasificacion IN ('estado', 'distrito', 'circunscripcion')

organization — Political Organizations

Stores political parties, legislative chambers, coalitions, and government institutions. This table has no foreign keys.

ColumnTypeNullableDescription
idTEXTNO (PK)Primary key, prefix O01, O02
nombreTEXTNOOfficial name (UNIQUE)
abbrTEXTYESAbbreviation (MORENA, PAN, etc.)
clasificacionTEXTNOType: partido, bancada, coalicion, gobierno, institucion, otro
fundacionTEXTYESFounding date in ISO 8601
disolucionTEXTYESDissolution date in ISO 8601
clasificacion IN ('partido', 'bancada', 'coalicion', 'gobierno', 'institucion', 'otro')

person — Legislators

Core table for legislators (deputies and senators). Includes demographic data, seat type, internal factions, and vulnerability assessments.

ColumnTypeNullableDescription
idTEXTNO (PK)Primary key, prefix P01, P02
nombreTEXTNOFull name
fecha_nacimientoTEXTYESBirth date in ISO 8601
generoTEXTYESGender: M, F, NB
curul_tipoTEXTYESSeat type: mayoria_relativa, plurinominal, suplente
circunscripcionINTEGERYESElectoral circumscription number (1-5)
start_dateTEXTYESStart of legislative term
end_dateTEXTYESEnd of legislative term
corriente_internaTEXTYESInternal faction: Monreal, AMLO, Sheinbaum, institucionalista, or NULL
vulnerabilidadTEXTYESPolitical vulnerability: alta, media, baja, or NULL
observacionesTEXTYESFree-text notes
identifiers_jsonTEXTYESJSON object with external IDs
genero IN ('M', 'F', 'NB')
curul_tipo IN ('mayoria_relativa', 'plurinominal', 'suplente')
circunscripcion BETWEEN 1 AND 5
corriente_interna IS NULL OR corriente_interna IN ('Monreal', 'AMLO', 'Sheinbaum', 'institucionalista')
vulnerabilidad IS NULL OR vulnerabilidad IN ('alta', 'media', 'baja')

:::caution The nullable enum fields (corriente_interna, vulnerabilidad) use the pattern field IS NULL OR field IN(...). The older field IN('val1', 'val2', NULL) pattern is ineffective because NULL IN (...) evaluates to NULL (not FALSE), bypassing the constraint entirely. :::

:::tip The corriente_interna field tracks political factions within parties. This is particularly relevant for MORENA, where internal divisions (Monrealistas, Sheinbaumistas, etc.) significantly affect voting behavior. :::

membership — Person-Organization Relations

Links persons to organizations with role, date range, and optional coalition representation.

ColumnTypeNullableDescription
idTEXTNO (PK)Primary key, prefix M01, M02
person_idTEXTNOForeign key to person(id), ON DELETE CASCADE ON UPDATE CASCADE
org_idTEXTNOForeign key to organization(id), ON DELETE CASCADE ON UPDATE CASCADE
rolTEXTNORole: diputado, senador, suplente
labelTEXTYESHuman-readable description of the membership
start_dateTEXTNOStart date of membership
end_dateTEXTYESEnd date. NULL means currently active
on_behalf_ofTEXTYESForeign key to organization(id), ON DELETE RESTRICT ON UPDATE RESTRICT. Coalition member voting under another party

:::note The on_behalf_of column handles coalition representation. When a deputy from Party A was elected under a coalition with Party B, this field records the coalition entity. :::

post — Legislative Positions

Represents formal positions within organizations (e.g., committee chair, board secretary).

ColumnTypeNullableDescription
idTEXTNO (PK)Primary key, prefix T01, T02
org_idTEXTNOForeign key to organization(id), ON DELETE RESTRICT ON UPDATE RESTRICT
area_idTEXTYESForeign key to area(id), ON DELETE RESTRICT ON UPDATE RESTRICT for geographically bound posts
labelTEXTNOTitle of the position
start_dateTEXTNOStart date
end_dateTEXTYESEnd date. NULL means position is still held

motion — Legislative Initiatives

Stores legislative proposals including constitutional reforms, secondary laws, and ordinary legislation.

ColumnTypeNullableDescription
idTEXTNO (PK)Primary key, prefix Y01, Y02
textoTEXTNOFull text or summary of the initiative
clasificacionTEXTNOType: reforma_constitucional, ley_secundaria, ordinaria, otra
requirementTEXTNOVote threshold required: mayoria_simple, mayoria_calificada, unanime
resultTEXTYESOutcome: aprobada, rechazada, pendiente, retirada, or NULL
dateTEXTYESDate of the initiative
legislative_sessionTEXTYESLegislative session label, e.g. “LXVI Legislatura”
fuente_urlTEXTYESSource URL for the initiative
clasificacion IN ('reforma_constitucional', 'ley_secundaria', 'ordinaria', 'otra')
requirement IN ('mayoria_simple', 'mayoria_calificada', 'unanime')
result IS NULL OR result IN ('aprobada', 'rechazada', 'pendiente', 'retirada')

vote_event — Voting Events

Records voting sessions. Each vote event is tied to a motion and an organization (chamber).

ColumnTypeNullableDescription
idTEXTNO (PK)Primary key, prefix VE01, VE02
motion_idTEXTNOForeign key to motion(id), ON DELETE RESTRICT ON UPDATE RESTRICT
start_dateTEXTNODate and time of the vote
organization_idTEXTNOForeign key to organization(id), ON DELETE RESTRICT ON UPDATE RESTRICT (chamber)
resultTEXTYESOutcome: aprobada, rechazada, empate, or NULL
sitl_idINTEGERYESSITL (Sistema de Informacion Legislativa) voting ID
voter_countINTEGERYESNumber of voters recorded
legislaturaTEXTYESLegislature code: LX, LXI, …, LXVI
requirementTEXTYESVote threshold: mayoria_simple, mayoria_calificada, unanime, or NULL
source_idTEXTYESOriginal portal ID, used for deduplication
identifiers_jsonTEXTYESJSON with external identifiers
result IS NULL OR result IN ('aprobada', 'rechazada', 'empate')
requirement IS NULL OR requirement IN ('mayoria_simple', 'mayoria_calificada', 'unanime')

vote — Individual Votes

Records each individual legislator’s vote within a vote event.

ColumnTypeNullableDescription
idTEXTNO (PK)Primary key, prefix V01, V02
vote_event_idTEXTNOForeign key to vote_event(id), ON DELETE CASCADE ON UPDATE CASCADE
voter_idTEXTNOForeign key to person(id), ON DELETE RESTRICT ON UPDATE RESTRICT
optionTEXTNOVote cast: a_favor, en_contra, abstencion, ausente
groupTEXTYESParty affiliation at the time of the vote
option IN ('a_favor', 'en_contra', 'abstencion', 'ausente')

:::tip The group column captures party affiliation at the time of the vote, which may differ from the person’s current membership. This is essential for tracking party-switching legislators. :::

count — Vote Counts by Group

Aggregated vote tallies per option per group (party). Used for quick statistical queries without scanning individual vote rows.

ColumnTypeNullableDescription
idTEXTNO (PK)Primary key, prefix C01, C02
vote_event_idTEXTNOForeign key to vote_event(id), ON DELETE CASCADE ON UPDATE CASCADE
optionTEXTNOVote option: a_favor, en_contra, abstencion, ausente
valueINTEGERNOCount of votes for this option (>= 0)
group_idTEXTYESForeign key to organization(id), ON DELETE RESTRICT ON UPDATE RESTRICT. The party being tallied
option IN ('a_favor', 'en_contra', 'abstencion', 'ausente')
value >= 0

actor_externo — External Actors

Stores political actors outside the legislature who influence legislative outcomes: governors, mayors, former presidents, judges, party leaders.

ColumnTypeNullableDescription
idTEXTNO (PK)Primary key, prefix AE01, AE02
nombreTEXTNOFull name
tipoTEXTNOActor type: gobernador, alcalde, ex_presidente, dirigente, juez, otro
area_idTEXTYESForeign key to area(id), ON DELETE RESTRICT ON UPDATE RESTRICT. Geographic jurisdiction
start_dateTEXTYESStart of relevance period
end_dateTEXTYESEnd of relevance period
observacionesTEXTYESFree-text notes
tipo IN ('gobernador', 'alcalde', 'ex_presidente', 'dirigente', 'juez', 'otro')

relacion_poder — Power Networks

Polymorphic relationship table modeling political connections between any combination of persons, organizations, and external actors. This is a Popolo extension specific to this project.

ColumnTypeNullableDescription
idTEXTNO (PK)Primary key, prefix RP01, RP02
source_typeTEXTNOSource entity type: person, organization, actor_externo
source_idTEXTNOID of the source entity
target_typeTEXTNOTarget entity type: person, organization, actor_externo
target_idTEXTNOID of the target entity
tipoTEXTNORelationship type: lealtad, presion, influencia, familiar, clientelismo, conflicto, alianza
pesoINTEGERNOWeight/strength of the relationship (1-5)
start_dateTEXTYESStart of the relationship
end_dateTEXTYESEnd of the relationship
fuenteTEXTYESSource of the relationship data
notaTEXTYESAdditional notes
source_type IN ('person', 'organization', 'actor_externo')
target_type IN ('person', 'organization', 'actor_externo')
tipo IN ('lealtad', 'presion', 'influencia', 'familiar', 'clientelismo', 'conflicto', 'alianza')
peso BETWEEN 1 AND 5

:::note relacion_poder uses a polymorphic design: source_type/source_id and target_type/target_id can point to any of three tables. This trades SQL-level foreign key enforcement for flexibility in modeling complex political networks. :::

evento_politico — Political Events

Records significant political events that may affect legislative behavior or outcomes.

ColumnTypeNullableDescription
idTEXTNO (PK)Primary key, prefix EP01, EP02
fechaTEXTNODate of the event
tipoTEXTNOType of event (free text)
descripcionTEXTNODescription of the event
consecuenciaTEXTYESExpected or observed consequences
fuente_urlTEXTYESSource URL
motion_idTEXTYESForeign key to motion(id), ON DELETE RESTRICT ON UPDATE RESTRICT. Related legislative initiative

Indexes

The following 18 indexes are defined to optimize common query patterns, including 2 unique composite indexes for deduplication:

Index NameTableColumnsType
idx_membership_personmembershipperson_idStandard
idx_membership_orgmembershiporg_idStandard
idx_vote_event_motionvote_eventmotion_idStandard
idx_vote_event_sourcevote_eventsource_idStandard
idx_vote_event_legislaturavote_eventlegislaturaStandard
idx_vote_event_orgvote_eventorganization_idStandard
idx_vote_event_start_datevote_eventstart_dateStandard
idx_vote_votervotevoter_idStandard
idx_vote_eventvotevote_event_idStandard
idx_vote_uniquevotevote_event_id, voter_idUNIQUE
idx_count_eventcountvote_event_idStandard
idx_count_groupcountgroup_idStandard
idx_count_uniquecountvote_event_id, option, group_idUNIQUE
idx_relacion_sourcerelacion_podersource_type, source_idStandard
idx_relacion_targetrelacion_podertarget_type, target_idStandard
idx_relacion_tiporelacion_podertipoStandard
idx_person_corrientepersoncorriente_internaStandard
idx_actor_tipoactor_externotipoStandard

:::tip The unique composite indexes (idx_vote_unique and idx_count_unique) prevent duplicate records at the database level. idx_vote_unique ensures a legislator cannot vote twice in the same event, and idx_count_unique ensures one tally per option per group per event. :::

Triggers

Four triggers enforce date consistency between start_date and end_date fields. They use the SELECT CASE WHEN ... THEN RAISE(ABORT, ...) END pattern with a WHEN clause at the trigger level:

trg_person_dates

Fires BEFORE INSERT on person. Ensures end_date is not earlier than start_date when both are present.

CREATE TRIGGER trg_person_dates
BEFORE INSERT ON person
WHEN NEW.end_date IS NOT NULL AND NEW.start_date IS NOT NULL
BEGIN
    SELECT CASE WHEN NEW.end_date < NEW.start_date
    THEN RAISE(ABORT, 'end_date debe ser >= start_date en person')
    END;
END;

trg_person_dates_update

Same validation on UPDATE of person.

CREATE TRIGGER trg_person_dates_update
BEFORE UPDATE ON person
WHEN NEW.end_date IS NOT NULL AND NEW.start_date IS NOT NULL
BEGIN
    SELECT CASE WHEN NEW.end_date < NEW.start_date
    THEN RAISE(ABORT, 'end_date debe ser >= start_date en person')
    END;
END;

trg_membership_dates

Fires BEFORE INSERT on membership. Prevents memberships with inverted date ranges.

CREATE TRIGGER trg_membership_dates
BEFORE INSERT ON membership
WHEN NEW.end_date IS NOT NULL AND NEW.start_date IS NOT NULL
BEGIN
    SELECT CASE WHEN NEW.end_date < NEW.start_date
    THEN RAISE(ABORT, 'end_date debe ser >= start_date en membership')
    END;
END;

trg_membership_dates_update

Same validation on UPDATE of membership.

CREATE TRIGGER trg_membership_dates_update
BEFORE UPDATE ON membership
WHEN NEW.end_date IS NOT NULL AND NEW.start_date IS NOT NULL
BEGIN
    SELECT CASE WHEN NEW.end_date < NEW.start_date
    THEN RAISE(ABORT, 'end_date debe ser >= start_date en membership')
    END;
END;

Statistics

Current data volumes in the production database:

MetricValue
Individual votes~3,510,053
Vote events~9,437
Persons~4,840
Organizations~20+
Legislatures covered7 (LX through LXVI, 2006-2027)
Tests302 passing
Indexes18 (including 2 UNIQUE)
Foreign Keys14 (3 CASCADE + 11 RESTRICT)