Skip to content
Back to skills

Erd Expert

ASecurity

Designs entity-relationship schemas for the hotel domain (reservations, guests, rooms, rate plans, folios) on PostgreSQL, following Archipelago International's Sentec platform conventions — hotel_id LIST partitioning, naming, and required audit columns. Use when designing a new table or ERD for Sentec PMS, Sentec Booking Engine, or Sentec EMS, when reviewing a proposed schema against org conventions, or when modeling the reservation lifecycle or room-availability queries.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 16, 2026
ai-agentsrustgosqldocumentation

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 16, 2026

npx -y skills add shennawardana23/skillme --skill erd-expert --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Erd Expert?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Erd Expert
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/shennawardana23-erd-expert/badge)](https://www.skillsdirectory.com/skills/shennawardana23-erd-expert)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: erd-expert
description: Designs entity-relationship schemas for the hotel domain (reservations, guests, rooms, rate plans, folios) on PostgreSQL, following Archipelago International's Sentec platform conventions — hotel_id LIST partitioning, naming, and required audit columns. Use when designing a new table or ERD for Sentec PMS, Sentec Booking Engine, or Sentec EMS, when reviewing a proposed schema against org conventions, or when modeling the reservation lifecycle or room-availability queries.
license: Apache-2.0
metadata:
  version: "0.1.0"
---

# ERD Expert — Hotel Domain Schema Design

Entity-relationship design for the hotel domain, targeting PostgreSQL and
this organization's Sentec platform (Sentec PMS, Sentec Booking Engine,
Sentec EMS). For the mechanics of keeping queries and migrations
partition-safe once a table exists, see `postgres-hotel-partitioning` —
this skill covers designing the ERD itself; that one covers querying and
migrating it correctly afterward. For index/data-type choices beyond what's
listed here, see `postgres-patterns`.

## Core hotel-domain entities

| Entity | Partition key | Key relationships |
|---|---|---|
| `hotel` | — | root anchor; every partitioned table's `hotel_id` FKs here conceptually (not enforceable as a literal FK across partitions in all cases — validate in application logic) |
| `room` | `hotel_id` | belongs to a hotel, has a `room_type` |
| `reservation` | `hotel_id` | references guest, hotel, room; carries a status state machine |
| `guest` | `hotel_id` | PII-sensitive; has a `loyalty_tier` |
| `rate_plan` | `hotel_id` | pricing rules, scoped per channel |
| `channel` | — | small reference table: `OTA \| DIRECT \| GDS \| WHOLESALER` |
| `folio` | `hotel_id` | billing record linked to a reservation |

### Reservation state machine

```
PENDING → CONFIRMED → CHECKED_IN → CHECKED_OUT
        ↘ CANCELLED (from PENDING or CONFIRMED)
        ↘ NO_SHOW   (from CONFIRMED)
```

Model this as a `CHECK` constraint or enum on `status`, plus an audit
table (`reservation_status_history`) if the transition history itself
must be queryable — don't try to derive "when did this become CONFIRMED"
from `updated_at` on the mutable row alone.

## Required columns and conventions

Every table in the hotel domain carries these columns, matching the
convention already established for partitioned tables:

```sql
id          bigint      GENERATED ALWAYS AS IDENTITY,
hotel_id    integer     NOT NULL,   -- partition key (LIST partitioning)
created_at  timestamptz NOT NULL DEFAULT now(),
updated_at  timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (hotel_id, id)
```

`hotel_id` is `integer`, not `uuid` — this matches the partitioning
convention already in use (`postgres-hotel-partitioning`), where
partitions are declared per discrete hotel ID (`FOR VALUES IN (101)`).
Keep this consistent across every new table in the domain; introducing a
`uuid` hotel identifier on a new table would break the ability to join
it against every existing `hotel_id`-partitioned table without a lookup.

**Naming**: tables `snake_case` singular (`reservation`, not
`reservations`); primary keys `{table}_id` when referenced as a foreign
key elsewhere (`reservation.guest_id` referencing `guest.id`); every
table gets `created_at`/`updated_at`.

**Data types** (see `postgres-patterns` for the full rationale): rates
and money as `numeric(12,2)`; flexible per-channel metadata as `jsonb`;
status fields as `text` with a `CHECK` constraint (portable, easy to
extend) or a native `enum` (stricter, but every new value requires an
`ALTER TYPE` migration — prefer `CHECK` unless the value set is genuinely
frozen); soft delete via nullable `deleted_at` (`NULL` = active row).

### Partitioning rule

```sql
CREATE TABLE reservation (
    id         bigint GENERATED ALWAYS AS IDENTITY,
    hotel_id   integer NOT NULL,
    guest_id   bigint NOT NULL,
    room_id    bigint NOT NULL,
    status     text NOT NULL CHECK (status IN ('PENDING','CONFIRMED','CHECKED_IN','CHECKED_OUT','CANCELLED','NO_SHOW')),
    check_in   date NOT NULL,
    check_out  date NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    PRIMARY KEY (hotel_id, id)
) PARTITION BY LIST (hotel_id);

CREATE TABLE reservation_101 PARTITION OF reservation FOR VALUES IN (101);
CREATE TABLE reservation_102 PARTITION OF reservation FOR VALUES IN (102);
```

`hotel_id` must appear in the primary key (and in every unique
constraint) on a list-partitioned table — PostgreSQL requires the
partition key be part of any unique index. Every query against this
table must filter `hotel_id` explicitly for the planner to prune
partitions — see `postgres-hotel-partitioning` for the full rule and the
join-on-both-sides requirement when joining two partitioned tables (e.g.
`reservation` to `room`, both scoped by the same `hotel_id`).

### Index strategy for hotel-domain tables

- B-tree on foreign keys (`guest_id`, `room_id`) — composed with
  `hotel_id` as the leading column since that's in every query.
- BRIN on append-only time columns at scale (`created_at`, `check_in`,
  `check_out`) — cheap to maintain, effective once a partition has
  millions of rows in roughly time order.
- Partial index for hot operational paths: `WHERE status IN
  ('CONFIRMED','CHECKED_IN')` for the "currently active reservations"
  query that runs on every front-desk lookup.

## Common query patterns

**Active reservations for a hotel** (partition-pruned):

```sql
SELECT * FROM reservation
WHERE hotel_id = $1
  AND status IN ('CONFIRMED', 'CHECKED_IN')
  AND check_out >= now();
```

**Room availability** (no double-booking overlap check):

```sql
SELECT r.id, r.room_type
FROM room r
WHERE r.hotel_id = $1
  AND r.status = 'AVAILABLE'
  AND NOT EXISTS (
    SELECT 1 FROM reservation res
    WHERE res.hotel_id = $1
      AND res.room_id = r.id
      AND res.status IN ('CONFIRMED', 'CHECKED_IN')
      AND res.check_in < $3 AND res.check_out > $2
  );
```

The overlap predicate (`check_in < requested_check_out AND check_out >
requested_check_in`) is the standard interval-overlap test — get the
strict/non-strict inequality direction right, or a reservation ending
exactly on the requested check-in date will incorrectly block (or fail to
block) the new booking.

## Sentec product boundaries

- **Sentec PMS**: core property management — `hotel`, `room`,
  `reservation`, `folio`.
- **Sentec Booking Engine**: direct online booking — `channel`,
  `rate_plan`, availability queries against the same `room`/`reservation`
  tables.
- **Sentec EMS**: employee/staff management — a **separate schema**
  (staff, shifts, assignments); do not add EMS entities to the PMS ERD or
  vice versa, and do not assume EMS tables share the `hotel_id`
  partitioning scheme without confirming it against that schema's own
  design.

## Gotchas

- Forgetting `hotel_id` in a new table's primary key breaks list
  partitioning outright at `CREATE TABLE` time (Postgres raises an
  error) — this is a compile-time-equivalent catch, but only if the table
  is declared `PARTITION BY LIST` in the first place; a table someone
  forgot to partition won't error, it'll just silently violate the
  org-wide convention.
- A `channel` or other small reference table should generally **not** be
  partitioned by `hotel_id` — partitioning a table with a few dozen rows
  adds planning overhead for no pruning benefit.
- Room availability overlap queries are correctness-critical (a bug here
  double-books a room); write a test with an exact boundary case
  (requested stay starts exactly on an existing reservation's checkout
  date) before trusting the query.
- `deleted_at` soft-delete columns need every `SELECT` and every unique
  index to account for them (`WHERE deleted_at IS NULL`, or a partial
  unique index) — a plain unique constraint on `email` will reject a new
  guest reusing an email that belongs to a soft-deleted guest record.

## Real-world grounding

PostgreSQL's own documentation states the partition key must be part of
every unique, primary key, and exclusion constraint on a partitioned
table — this is not a house style choice, it is an engine-enforced
requirement, which is why every table in this ERD carries `hotel_id` as
the leading column of its primary key rather than an independent
surrogate key.

## Verification

- [ ] Every hotel-domain table has `id`, `hotel_id`, `created_at`, `updated_at`, and `PRIMARY KEY (hotel_id, id)`
- [ ] `hotel_id` is `integer`, consistent with existing partitioned tables — not introduced as `uuid`
- [ ] Money/rate columns are `numeric(12,2)`; flexible metadata is `jsonb`
- [ ] New tables that should be partitioned declare `PARTITION BY LIST (hotel_id)` with a partition per hotel
- [ ] Every query against the new table filters `hotel_id` explicitly (see `postgres-hotel-partitioning`)
- [ ] Reservation state machine transitions are constrained (CHECK or enum), not left as a free-text field
- [ ] EMS entities are kept in their own schema, not merged into the PMS/Booking Engine ERD

Files in this skill

  • SKILL.md8.9 KB
  • evals/evals.json2.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.