Skill

postgres-conventions

This skill should be used when working on PostgreSQL databases, writing SQL schemas, creating tables, designing database architecture, or reviewing PostgreSQL code.

From ccfg-postgresql
Install
1
Run in your terminal
$
npx claudepluginhub jsamuelsen11/claude-config --plugin ccfg-postgresql
Tool Access

This skill uses the workspace's default tool permissions.

Skill Content

PostgreSQL Conventions

These are comprehensive conventions for PostgreSQL database development, covering schema design, indexing strategies, data type selection, and query patterns. Following these conventions ensures optimal performance, data integrity, and maintainability across PostgreSQL 15+ environments.

Existing Repository Compatibility

When working with existing PostgreSQL databases and projects, always respect established conventions and patterns before applying these preferences.

  • Audit before changing: Review existing table definitions, data types, and index choices to understand the project's current state and historical decisions.
  • Serial column compatibility: If the project uses serial / bigserial columns, understand they are legacy. Do not convert to identity columns without coordinating with the team, as it requires careful migration and may affect sequences used by application code.
  • Timezone handling: If the project uses timestamp (without time zone), document the limitation but do not change without understanding how the application handles timezone conversions. Migration to timestamptz requires a table rewrite and application changes.
  • Collation consistency: Mixed collations across columns can cause implicit conversions and prevent index usage. Document inconsistencies but plan migrations carefully.
  • Extension dependencies: If the project relies on specific extension versions, coordinate upgrades with the team. Extension updates may change behavior or require data migration.
  • Backward compatibility: When suggesting improvements, provide migration paths and rollback procedures for production systems.

These conventions apply primarily to new schemas, new tables, and scaffold output. For existing systems, propose changes through proper change management processes.

Schema Design Rules

Naming Conventions

All identifiers must use lowercase snake_case. Never use quoted identifiers, camelCase, PascalCase, or UPPERCASE names.

-- CORRECT: snake_case everywhere
CREATE TABLE user_accounts (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    first_name text NOT NULL,
    last_name text NOT NULL,
    email_address text NOT NULL UNIQUE,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now()
);

-- WRONG: camelCase (requires quoting, error-prone)
CREATE TABLE "userAccounts" (
    "firstName" text,
    "lastName" text,
    "emailAddress" text
);

-- WRONG: Mixed case without quotes (PostgreSQL folds to lowercase anyway)
CREATE TABLE UserAccounts (
    FirstName text  -- Becomes "firstname", not what you intended
);

Table Naming Rules

  • Use plural nouns for tables: users, orders, products
  • Use singular nouns for enum/lookup tables: order_status, priority
  • Use verb_noun for junction tables: user_roles, order_items
  • Never prefix tables with tbl_ or similar Hungarian notation
-- CORRECT: Plural table names
CREATE TABLE users (...);
CREATE TABLE orders (...);
CREATE TABLE order_items (...);

-- WRONG: Singular table names (inconsistent with SQL convention)
CREATE TABLE user (...);       -- Also a reserved word!
CREATE TABLE order (...);      -- Also a reserved word!
CREATE TABLE order_item (...);

-- WRONG: Hungarian notation
CREATE TABLE tbl_users (...);
CREATE TABLE t_orders (...);

Constraint and Index Naming

Always name constraints and indexes explicitly using standard prefixes.

-- CORRECT: Named constraints with standard prefixes
CREATE TABLE orders (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    customer_id bigint NOT NULL,
    product_id bigint NOT NULL,
    quantity integer NOT NULL,
    unit_price numeric(15, 2) NOT NULL,
    total_amount numeric(15, 2) GENERATED ALWAYS AS (
        quantity * unit_price
    ) STORED,
    status text NOT NULL DEFAULT 'pending',
    created_at timestamptz NOT NULL DEFAULT now(),

    CONSTRAINT fk_orders_customer_id
        FOREIGN KEY (customer_id) REFERENCES customers(id),
    CONSTRAINT fk_orders_product_id
        FOREIGN KEY (product_id) REFERENCES products(id),
    CONSTRAINT chk_orders_quantity_positive
        CHECK (quantity > 0),
    CONSTRAINT chk_orders_unit_price_positive
        CHECK (unit_price > 0),
    CONSTRAINT chk_orders_status_valid
        CHECK (status IN ('pending', 'confirmed', 'shipped', 'delivered', 'cancelled'))
);

CREATE INDEX idx_orders_customer_id ON orders (customer_id);
CREATE INDEX idx_orders_product_id ON orders (product_id);
CREATE INDEX idx_orders_status ON orders (status) WHERE status != 'delivered';
CREATE UNIQUE INDEX uq_orders_customer_product ON orders (customer_id, product_id)
    WHERE status = 'pending';

-- WRONG: Unnamed constraints (auto-generated names are hard to reference)
CREATE TABLE orders (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    customer_id bigint REFERENCES customers(id),  -- Auto-named FK
    quantity integer CHECK (quantity > 0)           -- Auto-named CHECK
);

CREATE INDEX ON orders (customer_id);  -- Auto-named index

Naming prefix conventions:

PrefixUsageExample
pk_Primary keypk_orders
fk_Foreign keyfk_orders_customer_id
idx_Indexidx_orders_customer_id
uq_Unique constraintuq_users_email
chk_Check constraintchk_orders_quantity_positive
excl_Exclusion constraintexcl_reservations_room_during
trg_Triggertrg_orders_updated_at

Timestamp Columns

Every mutable table should have created_at and updated_at columns.

-- CORRECT: Timestamp columns with defaults
CREATE TABLE orders (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    -- ... other columns ...
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now()
);

-- Auto-update updated_at with trigger
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS trigger AS $$
BEGIN
    NEW.updated_at = now();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_orders_updated_at
    BEFORE UPDATE ON orders
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

-- WRONG: Missing timestamps
CREATE TABLE orders (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    customer_id bigint NOT NULL
    -- No created_at, no updated_at
);

-- WRONG: Using timestamp instead of timestamptz
CREATE TABLE orders (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    created_at timestamp NOT NULL DEFAULT now()  -- Missing timezone!
);

Data Type Rules

Primary Keys: Identity Columns

Always use bigint GENERATED ALWAYS AS IDENTITY for primary keys. Never use serial or bigserial (legacy syntax). Use uuid only when there is a specific need for distributed ID generation.

-- CORRECT: Identity column (PostgreSQL 10+, SQL standard)
CREATE TABLE users (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY
);

-- CORRECT: UUID primary key (for distributed systems)
CREATE TABLE distributed_events (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid()
);

-- WRONG: serial (legacy, creates implicit sequence with different ownership)
CREATE TABLE users (
    id serial PRIMARY KEY
);

-- WRONG: bigserial (legacy)
CREATE TABLE users (
    id bigserial PRIMARY KEY
);

-- WRONG: integer identity (will overflow on high-traffic tables)
CREATE TABLE users (
    id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY
    -- integer max: 2,147,483,647 -- seems large but high-traffic tables hit this
);

Text Types: Always text

Use text for all string columns. Use varchar(n) only when you have a genuine business requirement for maximum length enforcement. Never use char(n).

-- CORRECT: text for all string columns
CREATE TABLE products (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name text NOT NULL,
    description text,
    sku text NOT NULL UNIQUE
);

-- CORRECT: varchar(n) when length limit is a business rule
CREATE TABLE countries (
    code varchar(3) PRIMARY KEY,  -- ISO 3166-1 alpha-3 code, always 3 chars
    name text NOT NULL
);

-- WRONG: varchar(255) cargo cult
CREATE TABLE products (
    name varchar(255) NOT NULL,        -- Why 255? No business reason.
    description varchar(1000)           -- Arbitrary limit
);

-- WRONG: char(n) (pads with spaces, wastes storage, confusing equality)
CREATE TABLE products (
    sku char(10) NOT NULL  -- Padded with spaces: 'ABC       '
);

Numeric Types: numeric for Money

Never use float, real, or double precision for monetary values or any value requiring exact decimal representation. Use numeric(precision, scale).

-- CORRECT: numeric for money
CREATE TABLE line_items (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    quantity integer NOT NULL CHECK (quantity > 0),
    unit_price numeric(15, 2) NOT NULL CHECK (unit_price >= 0),
    tax_rate numeric(5, 4) NOT NULL DEFAULT 0.0000,
    total numeric(15, 2) GENERATED ALWAYS AS (
        quantity * unit_price * (1 + tax_rate)
    ) STORED
);

-- CORRECT: integer for cents (avoids decimal arithmetic entirely)
CREATE TABLE transactions (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    amount_cents bigint NOT NULL CHECK (amount_cents > 0),
    currency text NOT NULL DEFAULT 'USD'
);

-- WRONG: float for money (introduces rounding errors)
CREATE TABLE bad_line_items (
    unit_price float NOT NULL,    -- 0.1 + 0.2 != 0.3 in float
    total double precision        -- Same problem
);

-- WRONG: money type (locale-dependent, limited precision, poor portability)
CREATE TABLE bad_transactions (
    amount money NOT NULL  -- Don't use the money type
);

Boolean Type

Use boolean for true/false values. Never use integer (0/1) or text ('Y'/'N').

-- CORRECT: boolean type
CREATE TABLE users (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    is_active boolean NOT NULL DEFAULT true,
    email_verified boolean NOT NULL DEFAULT false,
    is_admin boolean NOT NULL DEFAULT false
);

-- WRONG: integer for boolean
CREATE TABLE bad_users (
    is_active integer NOT NULL DEFAULT 1,   -- 0/1 is not boolean
    email_verified int DEFAULT 0             -- Allows values like 42
);

-- WRONG: text for boolean
CREATE TABLE bad_users (
    is_active text NOT NULL DEFAULT 'Y'  -- Allows 'Y', 'y', 'yes', 'YES', ...
);

Timestamp Type: Always timestamptz

Use timestamptz (timestamp with time zone) for all temporal data. Never use timestamp (without time zone) unless you have a specific reason (rare).

-- CORRECT: timestamptz for all time columns
CREATE TABLE events (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    event_time timestamptz NOT NULL DEFAULT now(),
    scheduled_at timestamptz,
    completed_at timestamptz
);

-- CORRECT: date for date-only values (no time component)
CREATE TABLE holidays (
    holiday_date date NOT NULL PRIMARY KEY,
    name text NOT NULL
);

-- WRONG: timestamp without time zone
CREATE TABLE bad_events (
    event_time timestamp NOT NULL DEFAULT now()
    -- Loses timezone information, ambiguous interpretation
);

-- WRONG: integer/bigint for Unix timestamps
CREATE TABLE bad_events (
    event_time bigint NOT NULL  -- Epoch seconds: loses readability, no timezone
);

JSON Type: Always jsonb

Use jsonb for all JSON data. Never use json (stored as text, cannot be indexed efficiently, slower for most operations).

-- CORRECT: jsonb for flexible metadata
CREATE TABLE products (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name text NOT NULL,
    attributes jsonb NOT NULL DEFAULT '{}',
    tags jsonb NOT NULL DEFAULT '[]'
);

-- Index specific jsonb paths
CREATE INDEX idx_products_category ON products USING btree ((attributes->>'category'));
CREATE INDEX idx_products_attributes ON products USING gin (attributes);
CREATE INDEX idx_products_tags ON products USING gin (tags jsonb_path_ops);

-- WRONG: json type (slower, cannot be indexed with GIN)
CREATE TABLE bad_products (
    attributes json NOT NULL DEFAULT '{}'
);

-- WRONG: Storing structured data as text
CREATE TABLE bad_products (
    attributes text  -- Requires manual JSON parsing
);

Network Types

Use PostgreSQL's built-in network types for IP addresses and network ranges.

-- CORRECT: inet for IP addresses
CREATE TABLE access_log (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    client_ip inet NOT NULL,
    request_path text NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now()
);

-- CORRECT: cidr for network ranges
CREATE TABLE allowed_networks (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    network cidr NOT NULL,
    description text
);

-- Query: Find log entries from a specific subnet
SELECT * FROM access_log
WHERE client_ip << '192.168.1.0/24'::cidr;

-- WRONG: text for IP addresses
CREATE TABLE bad_access_log (
    client_ip text NOT NULL  -- No validation, no subnet queries
);

Array Types

Use PostgreSQL arrays for ordered lists of simple values. For complex collections, use a separate table.

-- CORRECT: Array for simple tag lists
CREATE TABLE articles (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    title text NOT NULL,
    tags text[] NOT NULL DEFAULT '{}',
    scores integer[] DEFAULT '{}'
);

-- GIN index for array contains queries
CREATE INDEX idx_articles_tags ON articles USING gin (tags);

-- Query: Articles with specific tag
SELECT * FROM articles WHERE tags @> ARRAY['postgresql'];

-- Query: Articles with any of these tags
SELECT * FROM articles WHERE tags && ARRAY['postgresql', 'database'];

-- WRONG: Comma-separated strings
CREATE TABLE bad_articles (
    tags text  -- 'postgresql,database,sql' -- Can't index or query efficiently
);

-- WRONG: Arrays for complex objects (use a join table instead)
CREATE TABLE bad_orders (
    item_names text[],  -- Loses item_id, price, quantity relationships
    item_prices numeric[]
);

Index Rules

B-tree (Default)

B-tree is the default index type. Use for equality and range queries on scalar types.

-- CORRECT: B-tree for equality and range
CREATE INDEX idx_orders_created_at ON orders (created_at);
CREATE INDEX idx_users_email ON users (email);

-- CORRECT: Composite index (column order matters!)
-- Supports: WHERE status = 'active' AND created_at > '2025-01-01'
-- Supports: WHERE status = 'active' (leftmost prefix)
-- Does NOT support: WHERE created_at > '2025-01-01' alone (efficiently)
CREATE INDEX idx_orders_status_created ON orders (status, created_at);

-- CORRECT: Covering index (INCLUDE columns stored in leaf pages, PG 11+)
CREATE INDEX idx_orders_status_covering ON orders (status)
    INCLUDE (customer_id, total_amount);
-- Index-only scan: no heap fetches for covered columns

Partial Indexes

Create indexes that cover only a subset of rows. Dramatically smaller and faster for targeted queries.

-- CORRECT: Partial index for active records only
CREATE INDEX idx_orders_pending ON orders (created_at)
    WHERE status = 'pending';

-- CORRECT: Partial index for non-null values
CREATE INDEX idx_users_phone ON users (phone)
    WHERE phone IS NOT NULL;

-- CORRECT: Partial unique index (unique only within a subset)
CREATE UNIQUE INDEX uq_users_active_email ON users (email)
    WHERE deleted_at IS NULL;

-- Query that benefits from partial index
SELECT * FROM orders WHERE status = 'pending' ORDER BY created_at;
-- Uses idx_orders_pending (much smaller than full index)

Expression Indexes

Index computed expressions for queries that filter on transformations.

-- CORRECT: Expression index for case-insensitive search
CREATE INDEX idx_users_email_lower ON users (lower(email));

-- Query that uses it
SELECT * FROM users WHERE lower(email) = lower('User@Example.com');

-- CORRECT: Expression index on jsonb path
CREATE INDEX idx_products_category ON products ((attributes->>'category'));

-- CORRECT: Expression index for date extraction
CREATE INDEX idx_orders_year_month ON orders (
    date_trunc('month', created_at)
);

GIN Indexes

GIN (Generalized Inverted Index) is optimal for multi-valued types: arrays, jsonb, full-text search, and trigram matching.

-- CORRECT: GIN for jsonb containment
CREATE INDEX idx_products_attributes ON products USING gin (attributes);
-- Supports: WHERE attributes @> '{"color": "red"}'

-- CORRECT: GIN with jsonb_path_ops (smaller, supports @> only)
CREATE INDEX idx_products_attrs_path ON products
    USING gin (attributes jsonb_path_ops);

-- CORRECT: GIN for array containment
CREATE INDEX idx_articles_tags ON articles USING gin (tags);
-- Supports: WHERE tags @> ARRAY['postgresql']

-- CORRECT: GIN for full-text search
CREATE INDEX idx_articles_search ON articles USING gin (search_vector);
-- Supports: WHERE search_vector @@ to_tsquery('postgresql & database')

GiST Indexes

GiST (Generalized Search Tree) supports spatial data, range types, and nearest-neighbor queries.

-- CORRECT: GiST for range types
CREATE INDEX idx_reservations_during ON reservations USING gist (during);
-- Supports: WHERE during && '[2025-03-01, 2025-03-05)'::tstzrange

-- CORRECT: GiST for spatial data (PostGIS)
CREATE INDEX idx_locations_coords ON locations USING gist (coords);

-- CORRECT: GiST for exclusion constraints
ALTER TABLE reservations ADD CONSTRAINT excl_reservations_room_during
    EXCLUDE USING gist (room_id WITH =, during WITH &&);

BRIN Indexes

BRIN (Block Range Index) is extremely compact and efficient for naturally ordered data (e.g., time-series, append-only logs). Tiny index size but only useful when physical row order correlates with column values.

-- CORRECT: BRIN for time-series data (naturally ordered by insert time)
CREATE INDEX idx_events_created_brin ON events
    USING brin (created_at) WITH (pages_per_range = 32);

-- CORRECT: BRIN for append-only log tables
CREATE INDEX idx_audit_log_id_brin ON audit_log
    USING brin (id) WITH (pages_per_range = 64);

-- WRONG: BRIN on randomly-ordered column (poor correlation, useless index)
CREATE INDEX idx_users_email_brin ON users USING brin (email);

Hash Indexes

Hash indexes support only equality comparisons. Useful for large values where B-tree overhead is significant. PostgreSQL 10+ makes hash indexes WAL-logged and crash-safe.

-- CORRECT: Hash index for equality-only lookups on large values
CREATE INDEX idx_sessions_token_hash ON sessions USING hash (session_token);
-- Only supports: WHERE session_token = 'abc123...'

-- B-tree is almost always better unless the indexed values are very large
-- and you only ever do equality checks

Query Pattern Rules

Common Table Expressions (CTEs)

PostgreSQL 12+ does not fence CTEs by default (they may be inlined). Use MATERIALIZED or NOT MATERIALIZED hints when you need explicit control.

-- CORRECT: Let the optimizer decide (PostgreSQL 12+ default)
WITH active_users AS (
    SELECT id, email FROM users WHERE is_active = true
)
SELECT au.email, count(o.id) AS order_count
FROM active_users au
JOIN orders o ON o.user_id = au.id
GROUP BY au.email;

-- CORRECT: Force materialization (when CTE is referenced multiple times)
WITH MATERIALIZED user_stats AS (
    SELECT user_id, count(*) AS order_count, sum(total) AS total_spent
    FROM orders
    GROUP BY user_id
)
SELECT * FROM user_stats WHERE order_count > 10
UNION ALL
SELECT * FROM user_stats WHERE total_spent > 1000;

-- CORRECT: Prevent materialization (when optimizer doesn't push predicates)
WITH NOT MATERIALIZED recent_orders AS (
    SELECT * FROM orders WHERE created_at > now() - interval '30 days'
)
SELECT * FROM recent_orders WHERE customer_id = 42;
-- Predicate on customer_id is pushed into the CTE scan

Pagination

Use keyset pagination for consistent performance on large datasets. Avoid OFFSET for deep pagination.

-- CORRECT: Keyset pagination (constant performance regardless of page depth)
SELECT id, name, created_at
FROM products
WHERE (created_at, id) < ($last_created_at, $last_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- WRONG: OFFSET pagination (scans and discards rows, slower for deep pages)
SELECT id, name, created_at
FROM products
ORDER BY created_at DESC
OFFSET 10000 LIMIT 20;
-- Must scan 10020 rows and discard 10000

Bulk Operations

Use batch operations for large data modifications. Never update millions of rows in a single transaction.

-- CORRECT: Batch update with controlled transaction size
DO $$
DECLARE
    batch_size constant integer := 5000;
    rows_affected integer;
BEGIN
    LOOP
        UPDATE users
        SET status = 'inactive'
        WHERE id IN (
            SELECT id FROM users
            WHERE last_login_at < now() - interval '1 year'
              AND status = 'active'
            LIMIT batch_size
            FOR UPDATE SKIP LOCKED
        );
        GET DIAGNOSTICS rows_affected = ROW_COUNT;
        RAISE NOTICE 'Updated % rows', rows_affected;
        COMMIT;
        EXIT WHEN rows_affected < batch_size;
        PERFORM pg_sleep(0.1);  -- Brief pause to reduce lock contention
    END LOOP;
END $$;

-- WRONG: Single massive UPDATE (locks entire table, bloats WAL, blocks VACUUM)
UPDATE users SET status = 'inactive'
WHERE last_login_at < now() - interval '1 year';

UPSERT Pattern

Use INSERT ... ON CONFLICT for atomic upsert operations.

-- CORRECT: Upsert with ON CONFLICT
INSERT INTO product_inventory (product_id, warehouse_id, quantity, updated_at)
VALUES ($1, $2, $3, now())
ON CONFLICT (product_id, warehouse_id)
DO UPDATE SET
    quantity = EXCLUDED.quantity,
    updated_at = EXCLUDED.updated_at;

-- CORRECT: Upsert with DO NOTHING (ignore duplicates)
INSERT INTO user_logins (user_id, login_date)
VALUES ($1, current_date)
ON CONFLICT (user_id, login_date) DO NOTHING;

-- WRONG: Check-then-insert (race condition)
-- SELECT count(*) FROM products WHERE sku = $1;
-- IF count = 0 THEN INSERT ... END IF;
-- Another session can insert between SELECT and INSERT

Window Functions

Use window functions for ranking, running totals, and row comparisons without self-joins.

-- CORRECT: Running total with window function
SELECT order_date,
       amount,
       sum(amount) OVER (ORDER BY order_date) AS running_total,
       avg(amount) OVER (
           ORDER BY order_date
           ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
       ) AS moving_avg_7d
FROM daily_revenue;

-- CORRECT: Row numbering for deduplication
DELETE FROM users
WHERE id IN (
    SELECT id FROM (
        SELECT id, ROW_NUMBER() OVER (
            PARTITION BY email ORDER BY created_at DESC
        ) AS rn
        FROM users
    ) dupes
    WHERE rn > 1
);

Anti-Pattern Summary

Anti-PatternCorrect Approach
serial / bigserialbigint GENERATED ALWAYS AS IDENTITY
varchar(255)text
float for moneynumeric(p, s)
timestamptimestamptz
jsonjsonb
integer for booleanboolean
char(n)text
Unnamed constraintsfk_, idx_, chk_, uq_ prefixes
OFFSET paginationKeyset pagination
Single massive UPDATEBatched updates with LIMIT
EAV tablesjsonb columns or proper normalization
Polymorphic FKSeparate nullable FKs with CHECK
Missing created_atAlways include created_at and updated_at
camelCase identifierssnake_case identifiers
Stats
Parent Repo Stars0
Parent Repo Forks0
Last CommitFeb 10, 2026