March 12, 2021

When aerich or alembic auto-generated migration files aren’t enough.

Summary of the basic DDL/DML statements used to keep track of the Postrges DB migrations, followed by a couple of use cases.

General DDL / DML

CREATE / INSERT

CREATE EXTENSION IF NOT EXISTS postgis;
CREATE INDEX location_gidx ON product USING gist (location);
CREATE UNIQUE INDEX product_name_supplier_uidx ON product (name, supplier);
CREATE TYPE orderstatus AS ENUM('PROCESSED', 'SHIPPED', 'DELIVERED', 'CANCELLED');
-- upsert
INSERT INTO product (name, supplier_id, location, status) 
    VALUES ('NOTEBOOK-A6', 'Muji', ST_GeomFromText('POINT(12.3 4.56)', 4326), 'SHIPPED')
    ON CONFLICT ON CONSTRAINT product_name_supplier_uc DO 
    UPDATE SET location=EXCLUDED.location, status=EXCLUDED.status;

ALTER / UPDATE

ALTER SEQUENCE product_id_seq RESTART WITH 1453;
ALTER TABLE product ADD CONSTRAINT product_name_supplier_uc UNIQUE USING INDEX product_name_supplier_uidx;
ALTER TABLE product ADD CONSTRAINT product_supplier_fk FOREIGN KEY (supplier_id) REFERENCES supplier (id) ON DELETE CASCADE;
ALTER TABLE product ALTER COLUMN description TYPE VARCHAR(2000) USING description::VARCHAR(2000); --longer varchar
ALTER TABLE product ALTER COLUMN status TYPE orderstatus USING status::text::orderstatus;
ALTER TYPE orderstatus ADD VALUE 'OUT FOR DELIVERY';
ALTER TYPE orderstatus RENAME TO orderstatus_tmp;
UPDATE product SET orderstatus = NULL WHERE status = 'OUT FOR DELIVERY';

DROP / DELETE

DROP DATABASE my_db;
DROP INDEX product_name_supplier_uid;
ALTER TABLE product DROP CONSTRAINT product_name_supplier_uc;
DROP TYPE orderstatus_tmp;
DELETE FROM product WHERE id=1234;

Use cases

Add / delete column

--upgrade
ALTER TABLE product ADD COLUMN price NUMERIC NOT NULL CONSTRAINT positive_price CHECK (price > 0);
--downgrade
ALTER TABLE product DROP COLUMN price;

Rename column

--upgrade
ALTER TABLE product RENAME COLUMN description TO details;
--downgrade
ALTER TABLE product RENAME COLUMN details TO description;

Change column type

Character length

--upgrade - add characters to description column
ALTER TABLE product ALTER COLUMN description TYPE VARCHAR(2000) USING description::VARCHAR(2000);
--downgrade - remove characters from description column
UPDATE product SET description = LEFT(description, 200) WHERE CHAR_LENGTH(description) > 200;
ALTER TABLE product ALTER COLUMN description TYPE VARCHAR(200) USING description::VARCHAR(200); 

Enum

--upgrade - add a value to orderstatus type
ALTER TYPE orderstatus ADD VALUE 'OUT FOR DELIVERY';
--downgrade - remove a value from orderstatus type
CREATE TYPE old_orderstatus AS ENUM('PROCESSED', 'SHIPPED', 'DELIVERED', 'CANCELLED');
UPDATE product SET status = NULL WHERE status = 'OUT FOR DELIVERY';
ALTER TABLE product ALTER COLUMN status TYPE old_orderstatus USING status::text::old_orderstatus;
DROP TYPE orderstatus;
ALTER TYPE old_orderstatus RENAME TO orderstatus;

Add / delete index

--upgrade
CREATE INDEX location_gidx ON product USING gist (location);
CREATE UNIQUE INDEX product_name_supplier_uidx ON product (name, supplier);
--downgrade
DROP INDEX location_gidx;
DROP INDEX product_name_supplier_uidproduct_name_supplier_uidx

Add / delete constraint

--upgrade - add a unique and foreign key constraint
ALTER TABLE product ADD CONSTRAINT product_name_supplier_uc UNIQUE USING INDEX product_name_supplier_uidx;
ALTER TABLE product ADD CONSTRAINT product_supplier_fk FOREIGN KEY (supplier_id) REFERENCES supplier (id) ON DELETE CASCADE;
--downgrade - remove the unique and foreign key constraint
ALTER TABLE product DROP CONSTRAINT product_name_supplier_uc;
ALTER TABLE product DROP CONSTRAINT product_supplier_fk;

Create / delete table

--upgrade
CREATE TABLE IF NOT EXISTS "product" (
    "time_created" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    "time_updated" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    "id" SERIAL NOT NULL PRIMARY KEY,
    "name" VARCHAR(64) NOT NULL,
    "supplier_id" INT REFERENCES "supplier" ("id") ON DELETE CASCADE,
    "location" GEOMETRY(POINT,4326) NOT NULL,
    "properties" JSONB,
    "status" orderstatus NOT NULL DEFAULT 'processed',
    CONSTRAINT "uc_product_name_supplier" UNIQUE ("name", "supplier_id")
);
COMMENT ON COLUMN product.status IS 'PROCESSED: PROCESSED\nSHIPPED: SHIPPED\nDELIVERED: DELIVERED\nCANCELLED: CANCELLED';
--downgrade
DROP TABLE product;

Misc. Postgres commands

Unnesting an array column

-- Generate the table for testing

-- CREATE TABLE IF NOT EXISTS "intervals" (
--     "id" SERIAL NOT NULL PRIMARY KEY,
--     "cutoff" decimal[] NOT NULL
-- );
-- INSERT INTO intervals (id, cutoff) VALUES (1, '{0.0, 1.0, 2.5, 5.0, 10.0}');
-- INSERT INTO intervals (id, cutoff) VALUES (2, '{0.0, 10.0, 20.0, 50.0, 100.0, 250.0}');

SELECT
    id AS interval_id,
    GENERATE_SERIES(1, ARRAY_LENGTH(cutoff, 1)-1) AS range_id,
    UNNEST(cutoff[1:ARRAY_LENGTH(cutoff, 1)-1]) AS low,
    UNNEST(cutoff[2:ARRAY_LENGTH(cutoff, 1)]) AS high
FROM intervals

Looping through list of ids

This is useful to update a table with a foreign key.
As an example below we assume every user has ordered a specific product.

DO
$$
DECLARE
    f record;
BEGIN
    FOR f IN SELECT id FROM public.users
    LOOP 
	INSERT INTO order (orderstatus, product_id, user_id)
	VALUE ('PURCHASED', 123456, f.id);
    END LOOP;
END;
$$