diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c91f5a1..23e0dbc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -446,7 +446,12 @@ Database administrators use it to create the migration scripts.
**Learn-dev MPD Diagram**:
-> TODO: 
+> 
+
+The MPD is generated by [`tbls`](https://github.com/k1LoW/tbls) from the **live
+PostgreSQL database** (not from the MCD), so it always reflects the real schema.
+Run `make mpd` to regenerate it; per-table documentation is in
+`docs/database/merise/mpd/` (`public.
.md` / `.svg`).
#### ERD Diagram
diff --git a/Makefile b/Makefile
index 231d2d1..4172e12 100644
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,7 @@
# Display the syntax with available targets
help:
@echo "Available targets:"
- @echo " make diagrams — generate MCD, MLD, and MPD"
+ @echo " make diagrams — generate Merise MCD, MLD, and MPD diagrams"
@echo " make mcd — generate MCD"
@echo " make mld — generate MLD"
@echo " make mpd — generate MPD"
@@ -41,12 +41,25 @@ mld:
mocodo --input docs/database/merise/learn-dev_mld.mcd --output_dir docs/database/merise --colors ocean
@echo "MLD generated: docs/database/merise/learn-dev_mld.svg"
-# Generate MPD from the PostgreSQL database
+# Generate MPD from the PostgreSQL database.
+# Percent-encode the password because it may contain URL-reserved characters.
mpd:
@echo "Generating MPD from PostgreSQL Database..."
- @test -n "$(TBLS_DSN)" || (echo "TBLS_DSN is required (e.g., postgres://user:pass@host:5432/dbname)" >&2; exit 1)
- tbls doc "$(TBLS_DSN)" docs/database/merise --force
- @echo "MPD generated in docs/database/merise/"
+ @if [ -e .env ]; then \
+ while IFS= read -r line; do \
+ case "$$line" in [A-Za-z_]*=*) export "$$line";; esac; \
+ done < .env; \
+ fi; \
+ if [ -n "$$TBLS_DSN" ]; then \
+ DSN="$$TBLS_DSN"; \
+ elif [ -n "$$LEARNDEV_DB_USER" ]; then \
+ ENC_LEARNDEV_DB_PASSWORD=$$(printf '%s' "$$LEARNDEV_DB_PASSWORD" | python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read(), safe=""))'); \
+ DSN="postgres://$$LEARNDEV_DB_USER:$$ENC_LEARNDEV_DB_PASSWORD@$$POSTGRES_HOST:$$POSTGRES_PORT/$$LEARNDEV_DB_NAME?sslmode=disable"; \
+ else \
+ echo "TBLS_DSN is required (or define LEARNDEV_DB_* / POSTGRES_* in .env)" >&2; exit 1; \
+ fi; \
+ tbls doc "$$DSN" docs/database/merise/mpd --force
+ @echo "MPD generated in docs/database/merise/mpd/"
# Clean up generated diagram files
clean:
diff --git a/docs/database/merise/learn-dev.mcd b/docs/database/merise/learn-dev.mcd
index 7f8a71b..e2f7bc8 100644
--- a/docs/database/merise/learn-dev.mcd
+++ b/docs/database/merise/learn-dev.mcd
@@ -9,7 +9,7 @@
:
Email Token: token_id, token, expires_at, used_at
-Audit Log: log_id, action_type, entity_type, entity_id, description, ip_address, user_agent, was_successful, error_message, metadata
+Audit Log: log_id, action_type, entity_type, entity_id, description, ip_address, user_agent, was_successful, error_message, metadata, created_at
:
Reset Token: token_id, token, expires_at, used_at, ip_address
diff --git a/docs/database/merise/learn-dev.svg b/docs/database/merise/learn-dev.svg
index 448fe9a..7e3c097 100644
--- a/docs/database/merise/learn-dev.svg
+++ b/docs/database/merise/learn-dev.svg
@@ -1,99 +1,99 @@
-
-
+
+
-
-
+
+
-
-
-
-
- Receives
+
+
+
+
+ Receives
- 1,1
- 0,N
+ 1,1
+ 0,N
-
-
+
+
-
-
-
-
- Performs
+
+
+
+
+ Performs
- 0,1
- 0,N
+ 0,1
+ 0,N
-
-
+
+
-
-
-
-
- Requests
+
+
+
+
+ Requests
- 1,1
- 0,N
+ 1,1
+ 0,N
-
-
+
+
-
-
-
-
- Holds
- assigned_at
- assigned_by
+
+
+
+
+ Holds
+ assigned_at
+ assigned_by
- 0,N
- 0,N
+ 0,N
+ 0,N
-
-
-
-
+
+
+
+
- Email Token
- token_id
-
- token
- expires_at
- used_at
+ Email Token
+ token_id
+
+ token
+ expires_at
+ used_at
-
-
+
+
Audit Log
log_id
action_type
- entity_type
+ entity_type
entity_id
description
ip_address
@@ -101,62 +101,63 @@
was_successful
error_message
metadata
+ created_at
-
-
-
-
+
+
+
+
- Reset Token
- token_id
-
- token
- expires_at
- used_at
- ip_address
+ Reset Token
+ token_id
+
+ token
+ expires_at
+ used_at
+ ip_address
-
-
-
-
+
+
+
+
- User
- user_id
-
- username
- email
- password
- first_name
- last_name
- is_active
- is_verified
- is_locked
- failed_login_attempts
- last_login_at
- password_changed_at
+ User
+ user_id
+
+ username
+ email
+ password
+ first_name
+ last_name
+ is_active
+ is_verified
+ is_locked
+ failed_login_attempts
+ last_login_at
+ password_changed_at
-
-
-
-
+
+
+
+
- Role
- role_id
-
- role_name
- description
- is_active
+ Role
+ role_id
+
+ role_name
+ description
+ is_active
\ No newline at end of file
diff --git a/docs/database/merise/learn-dev_mld.svg b/docs/database/merise/learn-dev_mld.svg
index b999402..7e20c93 100644
--- a/docs/database/merise/learn-dev_mld.svg
+++ b/docs/database/merise/learn-dev_mld.svg
@@ -1,31 +1,31 @@
-
-
+
+
-
-
-
-
+
+
+
+
- Email Token
- token_id
-
- token
- expires_at
- used_at
- #user_id
+ Email Token
+ token_id
+
+ token
+ expires_at
+ used_at
+ #user_id
-
-
+
+
Audit Log
@@ -36,110 +36,111 @@
entity_id
description
ip_address
- user_agent
+ user_agent
was_successful
error_message
metadata
- #user_id
+ created_at
+ #user_id
-
-
-
-
+
+
+
+
- Reset Token
- token_id
-
- token
- expires_at
- used_at
- ip_address
- #user_id
+ Reset Token
+ token_id
+
+ token
+ expires_at
+ used_at
+ ip_address
+ #user_id
-
-
-
-
+
+
+
+
- User
- user_id
-
- username
- email
- password
- first_name
- last_name
- is_active
- is_verified
- is_locked
- failed_login_attempts
- last_login_at
- password_changed_at
+ User
+ user_id
+
+ username
+ email
+ password
+ first_name
+ last_name
+ is_active
+ is_verified
+ is_locked
+ failed_login_attempts
+ last_login_at
+ password_changed_at
-
-
-
-
+
+
+
+
- Holds
- #user_id
-
- #role_id
-
- assigned_at
- assigned_by
+ Holds
+ #user_id
+
+ #role_id
+
+ assigned_at
+ assigned_by
-
-
-
-
+
+
+
+
- Role
- role_id
-
- role_name
- description
- is_active
+ Role
+ role_id
+
+ role_name
+ description
+ is_active
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
\ No newline at end of file
diff --git a/docs/database/merise/mpd/README.md b/docs/database/merise/mpd/README.md
new file mode 100644
index 0000000..6b7473e
--- /dev/null
+++ b/docs/database/merise/mpd/README.md
@@ -0,0 +1,24 @@
+# learn-dev
+
+## Description
+
+Learn-dev MPD (Physical Data Model)
+
+## Tables
+
+| Name | Columns | Comment | Type |
+| ---- | ------- | ------- | ---- |
+| [public.users](public.users.md) | 12 | | BASE TABLE |
+| [public.roles](public.roles.md) | 4 | | BASE TABLE |
+| [public.user_roles](public.user_roles.md) | 4 | | BASE TABLE |
+| [public.email_tokens](public.email_tokens.md) | 5 | | BASE TABLE |
+| [public.reset_tokens](public.reset_tokens.md) | 6 | | BASE TABLE |
+| [public.audit_logs](public.audit_logs.md) | 12 | | BASE TABLE |
+
+## Relations
+
+
+
+---
+
+> Generated by [tbls](https://github.com/k1LoW/tbls)
diff --git a/docs/database/merise/mpd/public.audit_logs.md b/docs/database/merise/mpd/public.audit_logs.md
new file mode 100644
index 0000000..e9049bd
--- /dev/null
+++ b/docs/database/merise/mpd/public.audit_logs.md
@@ -0,0 +1,40 @@
+# public.audit_logs
+
+## Columns
+
+| Name | Type | Default | Nullable | Children | Parents | Comment |
+| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
+| log_id | bigint | | false | | | |
+| action_type | varchar(100) | | false | | | |
+| entity_type | varchar(100) | | true | | | |
+| entity_id | varchar(64) | | true | | | |
+| description | text | | true | | | |
+| ip_address | inet | | true | | | |
+| user_agent | text | | true | | | |
+| was_successful | boolean | true | false | | | |
+| error_message | text | | true | | | |
+| metadata | jsonb | | true | | | |
+| created_at | timestamp with time zone | now() | false | | | |
+| user_id | uuid | | true | | [public.users](public.users.md) | |
+
+## Constraints
+
+| Name | Type | Definition |
+| ---- | ---- | ---------- |
+| audit_logs_user_id_fkey | FOREIGN KEY | FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE SET NULL |
+| audit_logs_pkey | PRIMARY KEY | PRIMARY KEY (log_id) |
+
+## Indexes
+
+| Name | Definition |
+| ---- | ---------- |
+| audit_logs_pkey | CREATE UNIQUE INDEX audit_logs_pkey ON public.audit_logs USING btree (log_id) |
+| idx_audit_logs_user_id | CREATE INDEX idx_audit_logs_user_id ON public.audit_logs USING btree (user_id) |
+
+## Relations
+
+
+
+---
+
+> Generated by [tbls](https://github.com/k1LoW/tbls)
diff --git a/docs/database/merise/mpd/public.audit_logs.svg b/docs/database/merise/mpd/public.audit_logs.svg
new file mode 100644
index 0000000..93ee9ab
--- /dev/null
+++ b/docs/database/merise/mpd/public.audit_logs.svg
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+public.audit_logs
+
+
+
+public.audit_logs
+
+
+public.audit_logs
+
+[BASE TABLE]
+
+log_id
+[bigint]
+
+action_type
+[varchar(100)]
+
+entity_type
+[varchar(100)]
+
+entity_id
+[varchar(64)]
+
+description
+[text]
+
+ip_address
+[inet]
+
+user_agent
+[text]
+
+was_successful
+[boolean]
+
+error_message
+[text]
+
+metadata
+[jsonb]
+
+created_at
+[timestamp with time zone]
+
+user_id
+[uuid]
+
+
+
+
+public.users
+
+
+public.users
+
+[BASE TABLE]
+
+user_id
+[uuid]
+
+username
+[varchar(50)]
+
+email
+[varchar(255)]
+
+password
+[varchar(255)]
+
+first_name
+[varchar(100)]
+
+last_name
+[varchar(100)]
+
+is_active
+[boolean]
+
+is_verified
+[boolean]
+
+is_locked
+[boolean]
+
+failed_login_attempts
+[integer]
+
+last_login_at
+[timestamp with time zone]
+
+password_changed_at
+[timestamp with time zone]
+
+
+
+public.audit_logs:user_id->public.users:user_id
+
+
+
+
+
diff --git a/docs/database/merise/mpd/public.email_tokens.md b/docs/database/merise/mpd/public.email_tokens.md
new file mode 100644
index 0000000..0258555
--- /dev/null
+++ b/docs/database/merise/mpd/public.email_tokens.md
@@ -0,0 +1,35 @@
+# public.email_tokens
+
+## Columns
+
+| Name | Type | Default | Nullable | Children | Parents | Comment |
+| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
+| token_id | bigint | | false | | | |
+| token | varchar(255) | | false | | | |
+| expires_at | timestamp with time zone | | false | | | |
+| used_at | timestamp with time zone | | true | | | |
+| user_id | uuid | | false | | [public.users](public.users.md) | |
+
+## Constraints
+
+| Name | Type | Definition |
+| ---- | ---- | ---------- |
+| email_tokens_user_id_fkey | FOREIGN KEY | FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE |
+| email_tokens_pkey | PRIMARY KEY | PRIMARY KEY (token_id) |
+| email_tokens_token_key | UNIQUE | UNIQUE (token) |
+
+## Indexes
+
+| Name | Definition |
+| ---- | ---------- |
+| email_tokens_pkey | CREATE UNIQUE INDEX email_tokens_pkey ON public.email_tokens USING btree (token_id) |
+| email_tokens_token_key | CREATE UNIQUE INDEX email_tokens_token_key ON public.email_tokens USING btree (token) |
+| idx_email_tokens_user_id | CREATE INDEX idx_email_tokens_user_id ON public.email_tokens USING btree (user_id) |
+
+## Relations
+
+
+
+---
+
+> Generated by [tbls](https://github.com/k1LoW/tbls)
diff --git a/docs/database/merise/mpd/public.email_tokens.svg b/docs/database/merise/mpd/public.email_tokens.svg
new file mode 100644
index 0000000..af01e3b
--- /dev/null
+++ b/docs/database/merise/mpd/public.email_tokens.svg
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+public.email_tokens
+
+
+
+public.email_tokens
+
+
+public.email_tokens
+
+[BASE TABLE]
+
+token_id
+[bigint]
+
+token
+[varchar(255)]
+
+expires_at
+[timestamp with time zone]
+
+used_at
+[timestamp with time zone]
+
+user_id
+[uuid]
+
+
+
+
+public.users
+
+
+public.users
+
+[BASE TABLE]
+
+user_id
+[uuid]
+
+username
+[varchar(50)]
+
+email
+[varchar(255)]
+
+password
+[varchar(255)]
+
+first_name
+[varchar(100)]
+
+last_name
+[varchar(100)]
+
+is_active
+[boolean]
+
+is_verified
+[boolean]
+
+is_locked
+[boolean]
+
+failed_login_attempts
+[integer]
+
+last_login_at
+[timestamp with time zone]
+
+password_changed_at
+[timestamp with time zone]
+
+
+
+public.email_tokens:user_id->public.users:user_id
+
+
+
+
+
diff --git a/docs/database/merise/mpd/public.reset_tokens.md b/docs/database/merise/mpd/public.reset_tokens.md
new file mode 100644
index 0000000..b3f5689
--- /dev/null
+++ b/docs/database/merise/mpd/public.reset_tokens.md
@@ -0,0 +1,36 @@
+# public.reset_tokens
+
+## Columns
+
+| Name | Type | Default | Nullable | Children | Parents | Comment |
+| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
+| token_id | bigint | | false | | | |
+| token | varchar(255) | | false | | | |
+| expires_at | timestamp with time zone | | false | | | |
+| used_at | timestamp with time zone | | true | | | |
+| ip_address | inet | | true | | | |
+| user_id | uuid | | false | | [public.users](public.users.md) | |
+
+## Constraints
+
+| Name | Type | Definition |
+| ---- | ---- | ---------- |
+| reset_tokens_user_id_fkey | FOREIGN KEY | FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE |
+| reset_tokens_pkey | PRIMARY KEY | PRIMARY KEY (token_id) |
+| reset_tokens_token_key | UNIQUE | UNIQUE (token) |
+
+## Indexes
+
+| Name | Definition |
+| ---- | ---------- |
+| reset_tokens_pkey | CREATE UNIQUE INDEX reset_tokens_pkey ON public.reset_tokens USING btree (token_id) |
+| reset_tokens_token_key | CREATE UNIQUE INDEX reset_tokens_token_key ON public.reset_tokens USING btree (token) |
+| idx_reset_tokens_user_id | CREATE INDEX idx_reset_tokens_user_id ON public.reset_tokens USING btree (user_id) |
+
+## Relations
+
+
+
+---
+
+> Generated by [tbls](https://github.com/k1LoW/tbls)
diff --git a/docs/database/merise/mpd/public.reset_tokens.svg b/docs/database/merise/mpd/public.reset_tokens.svg
new file mode 100644
index 0000000..cb0a6b4
--- /dev/null
+++ b/docs/database/merise/mpd/public.reset_tokens.svg
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+public.reset_tokens
+
+
+
+public.reset_tokens
+
+
+public.reset_tokens
+
+[BASE TABLE]
+
+token_id
+[bigint]
+
+token
+[varchar(255)]
+
+expires_at
+[timestamp with time zone]
+
+used_at
+[timestamp with time zone]
+
+ip_address
+[inet]
+
+user_id
+[uuid]
+
+
+
+
+public.users
+
+
+public.users
+
+[BASE TABLE]
+
+user_id
+[uuid]
+
+username
+[varchar(50)]
+
+email
+[varchar(255)]
+
+password
+[varchar(255)]
+
+first_name
+[varchar(100)]
+
+last_name
+[varchar(100)]
+
+is_active
+[boolean]
+
+is_verified
+[boolean]
+
+is_locked
+[boolean]
+
+failed_login_attempts
+[integer]
+
+last_login_at
+[timestamp with time zone]
+
+password_changed_at
+[timestamp with time zone]
+
+
+
+public.reset_tokens:user_id->public.users:user_id
+
+
+
+
+
diff --git a/docs/database/merise/mpd/public.roles.md b/docs/database/merise/mpd/public.roles.md
new file mode 100644
index 0000000..13c7cf2
--- /dev/null
+++ b/docs/database/merise/mpd/public.roles.md
@@ -0,0 +1,32 @@
+# public.roles
+
+## Columns
+
+| Name | Type | Default | Nullable | Children | Parents | Comment |
+| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
+| role_id | bigint | | false | [public.user_roles](public.user_roles.md) | | |
+| role_name | varchar(50) | | false | | | |
+| description | varchar(255) | | true | | | |
+| is_active | boolean | true | false | | | |
+
+## Constraints
+
+| Name | Type | Definition |
+| ---- | ---- | ---------- |
+| roles_pkey | PRIMARY KEY | PRIMARY KEY (role_id) |
+| roles_role_name_key | UNIQUE | UNIQUE (role_name) |
+
+## Indexes
+
+| Name | Definition |
+| ---- | ---------- |
+| roles_pkey | CREATE UNIQUE INDEX roles_pkey ON public.roles USING btree (role_id) |
+| roles_role_name_key | CREATE UNIQUE INDEX roles_role_name_key ON public.roles USING btree (role_name) |
+
+## Relations
+
+
+
+---
+
+> Generated by [tbls](https://github.com/k1LoW/tbls)
diff --git a/docs/database/merise/mpd/public.roles.svg b/docs/database/merise/mpd/public.roles.svg
new file mode 100644
index 0000000..148bcf7
--- /dev/null
+++ b/docs/database/merise/mpd/public.roles.svg
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+public.roles
+
+
+
+public.roles
+
+
+public.roles
+
+[BASE TABLE]
+
+role_id
+[bigint]
+
+role_name
+[varchar(50)]
+
+description
+[varchar(255)]
+
+is_active
+[boolean]
+
+
+
+
+public.user_roles
+
+
+public.user_roles
+
+[BASE TABLE]
+
+user_id
+[uuid]
+
+role_id
+[bigint]
+
+assigned_at
+[timestamp with time zone]
+
+assigned_by
+[uuid]
+
+
+
+public.user_roles:role_id->public.roles:role_id
+
+
+
+
+
diff --git a/docs/database/merise/mpd/public.user_roles.md b/docs/database/merise/mpd/public.user_roles.md
new file mode 100644
index 0000000..60537b1
--- /dev/null
+++ b/docs/database/merise/mpd/public.user_roles.md
@@ -0,0 +1,34 @@
+# public.user_roles
+
+## Columns
+
+| Name | Type | Default | Nullable | Children | Parents | Comment |
+| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
+| user_id | uuid | | false | | [public.users](public.users.md) | |
+| role_id | bigint | | false | | [public.roles](public.roles.md) | |
+| assigned_at | timestamp with time zone | now() | false | | | |
+| assigned_by | uuid | | true | | [public.users](public.users.md) | |
+
+## Constraints
+
+| Name | Type | Definition |
+| ---- | ---- | ---------- |
+| user_roles_assigned_by_fkey | FOREIGN KEY | FOREIGN KEY (assigned_by) REFERENCES users(user_id) ON DELETE SET NULL |
+| user_roles_user_id_fkey | FOREIGN KEY | FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE |
+| user_roles_role_id_fkey | FOREIGN KEY | FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE |
+| user_roles_pkey | PRIMARY KEY | PRIMARY KEY (user_id, role_id) |
+
+## Indexes
+
+| Name | Definition |
+| ---- | ---------- |
+| user_roles_pkey | CREATE UNIQUE INDEX user_roles_pkey ON public.user_roles USING btree (user_id, role_id) |
+| idx_user_roles_role_id | CREATE INDEX idx_user_roles_role_id ON public.user_roles USING btree (role_id) |
+
+## Relations
+
+
+
+---
+
+> Generated by [tbls](https://github.com/k1LoW/tbls)
diff --git a/docs/database/merise/mpd/public.user_roles.svg b/docs/database/merise/mpd/public.user_roles.svg
new file mode 100644
index 0000000..f373d99
--- /dev/null
+++ b/docs/database/merise/mpd/public.user_roles.svg
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+public.user_roles
+
+
+
+public.user_roles
+
+
+public.user_roles
+
+[BASE TABLE]
+
+user_id
+[uuid]
+
+role_id
+[bigint]
+
+assigned_at
+[timestamp with time zone]
+
+assigned_by
+[uuid]
+
+
+
+
+public.users
+
+
+public.users
+
+[BASE TABLE]
+
+user_id
+[uuid]
+
+username
+[varchar(50)]
+
+email
+[varchar(255)]
+
+password
+[varchar(255)]
+
+first_name
+[varchar(100)]
+
+last_name
+[varchar(100)]
+
+is_active
+[boolean]
+
+is_verified
+[boolean]
+
+is_locked
+[boolean]
+
+failed_login_attempts
+[integer]
+
+last_login_at
+[timestamp with time zone]
+
+password_changed_at
+[timestamp with time zone]
+
+
+
+public.user_roles:user_id->public.users:user_id
+
+
+
+
+
+public.user_roles:assigned_by->public.users:user_id
+
+
+
+
+
+public.user_roles:assigned_by->public.users:user_id
+
+
+
+
+
+public.roles
+
+
+public.roles
+
+[BASE TABLE]
+
+role_id
+[bigint]
+
+role_name
+[varchar(50)]
+
+description
+[varchar(255)]
+
+is_active
+[boolean]
+
+
+
+public.user_roles:role_id->public.roles:role_id
+
+
+
+
+
diff --git a/docs/database/merise/mpd/public.users.md b/docs/database/merise/mpd/public.users.md
new file mode 100644
index 0000000..278ef47
--- /dev/null
+++ b/docs/database/merise/mpd/public.users.md
@@ -0,0 +1,42 @@
+# public.users
+
+## Columns
+
+| Name | Type | Default | Nullable | Children | Parents | Comment |
+| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
+| user_id | uuid | gen_random_uuid() | false | [public.user_roles](public.user_roles.md) [public.email_tokens](public.email_tokens.md) [public.reset_tokens](public.reset_tokens.md) [public.audit_logs](public.audit_logs.md) | | |
+| username | varchar(50) | | false | | | |
+| email | varchar(255) | | false | | | |
+| password | varchar(255) | | false | | | |
+| first_name | varchar(100) | | true | | | |
+| last_name | varchar(100) | | true | | | |
+| is_active | boolean | true | false | | | |
+| is_verified | boolean | false | false | | | |
+| is_locked | boolean | false | false | | | |
+| failed_login_attempts | integer | 0 | false | | | |
+| last_login_at | timestamp with time zone | | true | | | |
+| password_changed_at | timestamp with time zone | | true | | | |
+
+## Constraints
+
+| Name | Type | Definition |
+| ---- | ---- | ---------- |
+| users_pkey | PRIMARY KEY | PRIMARY KEY (user_id) |
+| users_username_key | UNIQUE | UNIQUE (username) |
+| users_email_key | UNIQUE | UNIQUE (email) |
+
+## Indexes
+
+| Name | Definition |
+| ---- | ---------- |
+| users_pkey | CREATE UNIQUE INDEX users_pkey ON public.users USING btree (user_id) |
+| users_username_key | CREATE UNIQUE INDEX users_username_key ON public.users USING btree (username) |
+| users_email_key | CREATE UNIQUE INDEX users_email_key ON public.users USING btree (email) |
+
+## Relations
+
+
+
+---
+
+> Generated by [tbls](https://github.com/k1LoW/tbls)
diff --git a/docs/database/merise/mpd/public.users.svg b/docs/database/merise/mpd/public.users.svg
new file mode 100644
index 0000000..2459378
--- /dev/null
+++ b/docs/database/merise/mpd/public.users.svg
@@ -0,0 +1,212 @@
+
+
+
+
+
+
+public.users
+
+
+
+public.users
+
+
+public.users
+
+[BASE TABLE]
+
+user_id
+[uuid]
+
+username
+[varchar(50)]
+
+email
+[varchar(255)]
+
+password
+[varchar(255)]
+
+first_name
+[varchar(100)]
+
+last_name
+[varchar(100)]
+
+is_active
+[boolean]
+
+is_verified
+[boolean]
+
+is_locked
+[boolean]
+
+failed_login_attempts
+[integer]
+
+last_login_at
+[timestamp with time zone]
+
+password_changed_at
+[timestamp with time zone]
+
+
+
+
+public.user_roles
+
+
+public.user_roles
+
+[BASE TABLE]
+
+user_id
+[uuid]
+
+role_id
+[bigint]
+
+assigned_at
+[timestamp with time zone]
+
+assigned_by
+[uuid]
+
+
+
+public.user_roles:assigned_by->public.users:user_id
+
+
+
+
+
+public.user_roles:user_id->public.users:user_id
+
+
+
+
+
+public.user_roles:assigned_by->public.users:user_id
+
+
+
+
+
+public.email_tokens
+
+
+public.email_tokens
+
+[BASE TABLE]
+
+token_id
+[bigint]
+
+token
+[varchar(255)]
+
+expires_at
+[timestamp with time zone]
+
+used_at
+[timestamp with time zone]
+
+user_id
+[uuid]
+
+
+
+public.email_tokens:user_id->public.users:user_id
+
+
+
+
+
+public.reset_tokens
+
+
+public.reset_tokens
+
+[BASE TABLE]
+
+token_id
+[bigint]
+
+token
+[varchar(255)]
+
+expires_at
+[timestamp with time zone]
+
+used_at
+[timestamp with time zone]
+
+ip_address
+[inet]
+
+user_id
+[uuid]
+
+
+
+public.reset_tokens:user_id->public.users:user_id
+
+
+
+
+
+public.audit_logs
+
+
+public.audit_logs
+
+[BASE TABLE]
+
+log_id
+[bigint]
+
+action_type
+[varchar(100)]
+
+entity_type
+[varchar(100)]
+
+entity_id
+[varchar(64)]
+
+description
+[text]
+
+ip_address
+[inet]
+
+user_agent
+[text]
+
+was_successful
+[boolean]
+
+error_message
+[text]
+
+metadata
+[jsonb]
+
+created_at
+[timestamp with time zone]
+
+user_id
+[uuid]
+
+
+
+public.audit_logs:user_id->public.users:user_id
+
+
+
+
+
diff --git a/docs/database/merise/mpd/schema.svg b/docs/database/merise/mpd/schema.svg
new file mode 100644
index 0000000..3a08b89
--- /dev/null
+++ b/docs/database/merise/mpd/schema.svg
@@ -0,0 +1,238 @@
+
+
+
+
+
+
+learn-dev
+
+
+
+public.users
+
+
+public.users
+
+[BASE TABLE]
+
+user_id
+[uuid]
+
+username
+[varchar(50)]
+
+email
+[varchar(255)]
+
+password
+[varchar(255)]
+
+first_name
+[varchar(100)]
+
+last_name
+[varchar(100)]
+
+is_active
+[boolean]
+
+is_verified
+[boolean]
+
+is_locked
+[boolean]
+
+failed_login_attempts
+[integer]
+
+last_login_at
+[timestamp with time zone]
+
+password_changed_at
+[timestamp with time zone]
+
+
+
+public.roles
+
+
+public.roles
+
+[BASE TABLE]
+
+role_id
+[bigint]
+
+role_name
+[varchar(50)]
+
+description
+[varchar(255)]
+
+is_active
+[boolean]
+
+
+
+public.user_roles
+
+
+public.user_roles
+
+[BASE TABLE]
+
+user_id
+[uuid]
+
+role_id
+[bigint]
+
+assigned_at
+[timestamp with time zone]
+
+assigned_by
+[uuid]
+
+
+
+public.user_roles:assigned_by->public.users:user_id
+
+
+
+
+
+public.user_roles:user_id->public.users:user_id
+
+
+
+
+
+public.user_roles:assigned_by->public.users:user_id
+
+
+
+
+
+public.user_roles:role_id->public.roles:role_id
+
+
+
+
+
+public.email_tokens
+
+
+public.email_tokens
+
+[BASE TABLE]
+
+token_id
+[bigint]
+
+token
+[varchar(255)]
+
+expires_at
+[timestamp with time zone]
+
+used_at
+[timestamp with time zone]
+
+user_id
+[uuid]
+
+
+
+public.email_tokens:user_id->public.users:user_id
+
+
+
+
+
+public.reset_tokens
+
+
+public.reset_tokens
+
+[BASE TABLE]
+
+token_id
+[bigint]
+
+token
+[varchar(255)]
+
+expires_at
+[timestamp with time zone]
+
+used_at
+[timestamp with time zone]
+
+ip_address
+[inet]
+
+user_id
+[uuid]
+
+
+
+public.reset_tokens:user_id->public.users:user_id
+
+
+
+
+
+public.audit_logs
+
+
+public.audit_logs
+
+[BASE TABLE]
+
+log_id
+[bigint]
+
+action_type
+[varchar(100)]
+
+entity_type
+[varchar(100)]
+
+entity_id
+[varchar(64)]
+
+description
+[text]
+
+ip_address
+[inet]
+
+user_agent
+[text]
+
+was_successful
+[boolean]
+
+error_message
+[text]
+
+metadata
+[jsonb]
+
+created_at
+[timestamp with time zone]
+
+user_id
+[uuid]
+
+
+
+public.audit_logs:user_id->public.users:user_id
+
+
+
+
+
diff --git a/tbls.yaml b/tbls.yaml
new file mode 100644
index 0000000..5650a2b
--- /dev/null
+++ b/tbls.yaml
@@ -0,0 +1,42 @@
+# ~~~ Database name of the document
+#
+name: learn-dev
+
+# ~~~ Database description
+#
+desc: Learn-dev MPD (Physical Data Model)
+
+# ~~~ Do not generate JSON schema file
+#
+disableOutputSchema: true
+
+# ~~~ Hide Liquibase tables
+#
+exclude: [databasechangelog, databasechangeloglock]
+
+# ~~~ Relations
+# Help tbls infer the self-reference on user_roles.assigned_by,
+# which it cannot detect from foreign keys alone.
+#
+relations:
+ - table: user_roles
+ columns:
+ - assigned_by
+ parentTable: users
+ parentColumns:
+ - user_id
+ def: admin who assigned the role
+
+er:
+ # ER diagram image format (`png`, `jpg`, `svg`, `mermaid`)
+ # Default is `svg`
+ format: svg
+ # Add table/column comment to ER diagram
+ # Default is false
+ comment: true
+ # Hide relation definition from ER diagram
+ # Default is false
+ hideDef: true
+ # Distance between tables that display relations in the ER
+ # Default is 1 (only directly-related tables appear in each table's diagram)
+ distance: 1