pgtofu’s declarative approach lets you define your desired database schema in SQL files. Instead of writing incremental migrations, you describe what your schema should look like, and pgtofu figures out how to get there.
CREATE TABLE users ( -- Identity column (PostgreSQL 10+) id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- Standard columns email VARCHAR(255) NOT NULL, name VARCHAR(100), -- Timestamps with defaults created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Constraints CONSTRAINT users_email_unique UNIQUE (email));-- Separate index definitionCREATE INDEX idx_users_created_at ON users(created_at);-- Partial indexCREATE INDEX idx_users_active ON users(email) WHERE status = 'active';
CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, total NUMERIC(12, 2) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());-- Or with explicit constraintCREATE TABLE order_items ( id BIGSERIAL PRIMARY KEY, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL, CONSTRAINT fk_order_items_order FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE, CONSTRAINT fk_order_items_product FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT);
-- PL/pgSQL functionCREATE OR REPLACE FUNCTION update_updated_at()RETURNS TRIGGER AS $$BEGIN NEW.updated_at = NOW(); RETURN NEW;END;$$ LANGUAGE plpgsql;-- SQL functionCREATE OR REPLACE FUNCTION get_user_order_count(user_id BIGINT)RETURNS INT AS $$ SELECT COUNT(*)::INT FROM orders WHERE user_id = $1;$$ LANGUAGE sql STABLE;
-- Enum typeCREATE TYPE order_status AS ENUM ('pending', 'confirmed', 'shipped', 'delivered');-- Composite typeCREATE TYPE address AS ( street VARCHAR(255), city VARCHAR(100), country VARCHAR(100), postal_code VARCHAR(20));-- Domain typeCREATE DOMAIN email_address AS VARCHAR(255) CHECK (VALUE ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
schema/├── extensions.sql # All extensions├── types.sql # All custom types├── tables/ # One file per table├── views/ # One file per view└── functions.sql # All functions
Pros: Easy to find specific object types
Cons: Related objects spread across files
-- schema/tables/users.sqlCREATE TABLE users (...);CREATE INDEX idx_users_email ON users(email);CREATE INDEX idx_users_created_at ON users(created_at);