stacker.news/prisma/migrations/20231201014953_territory/migration.sql

286 lines
11 KiB
PL/PgSQL

/*
Warnings:
- Added the required column `billingCost` to the `Sub` table without a default value. This is not possible if the table is not empty.
- Added the required column `billingType` to the `Sub` table without a default value. This is not possible if the table is not empty.
- Added the required column `userId` to the `Sub` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "BillingType" AS ENUM ('MONTHLY', 'YEARLY', 'ONCE');
-- CreateEnum
CREATE TYPE "SubActType" AS ENUM ('BILLING', 'REVENUE');
-- AlterEnum
ALTER TYPE "Status" ADD VALUE 'GRACE';
-- add new columns giving old columns default special default values
ALTER TABLE "Sub"
ADD COLUMN "billedLastAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "billingCost" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "billingType" "BillingType" NOT NULL DEFAULT 'ONCE',
ADD COLUMN "parentName" CITEXT,
ADD COLUMN "path" ltree,
ADD COLUMN "status" "Status" NOT NULL DEFAULT 'ACTIVE',
ADD COLUMN "userId" INTEGER NOT NULL DEFAULT 616,
ADD COLUMN "rewardsPct" INTEGER NOT NULL DEFAULT 100;
-- set the default values for the new columns from this point forward
ALTER TABLE "Sub"
ALTER COLUMN "billingCost" DROP DEFAULT,
ALTER COLUMN "billingType" DROP DEFAULT,
ALTER COLUMN "userId" DROP DEFAULT,
ALTER COLUMN "rewardsPct" SET DEFAULT 50;
-- constrain percent to be between 0 and 100
ALTER TABLE "Sub" ADD CONSTRAINT "rewardsPct" CHECK ("rewardsPct" >= 0 AND "rewardsPct" <= 100) NOT VALID;
-- we plan to structure subs as a tree
UPDATE "Sub" SET "path" = LOWER(name)::ltree;
-- assign subs to appropriate people
-- UPDATE "Sub" SET "userId" = 6030 WHERE name = 'tech';
-- UPDATE "Sub" SET "userId" = 4502 WHERE name = 'meta';
-- CreateTable
CREATE TABLE "SubAct" (
"id" SERIAL NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" INTEGER NOT NULL,
"subName" CITEXT NOT NULL,
"msats" BIGINT NOT NULL,
"type" "SubActType" NOT NULL,
CONSTRAINT "SubAct_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "SubAct_userId_idx" ON "SubAct"("userId");
-- CreateIndex
CREATE INDEX "SubAct_userId_type_idx" ON "SubAct"("userId", "type");
-- CreateIndex
CREATE INDEX "SubAct_type_idx" ON "SubAct"("type");
-- CreateIndex
CREATE INDEX "SubAct_created_at_idx" ON "SubAct"("created_at");
-- CreateIndex
CREATE INDEX "SubAct_created_at_type_idx" ON "SubAct"("created_at", "type");
-- CreateIndex
CREATE INDEX "SubAct_userId_created_at_type_idx" ON "SubAct"("userId", "created_at", "type");
-- CreateIndex
CREATE INDEX "Sub_parentName_idx" ON "Sub"("parentName");
-- CreateIndex
CREATE INDEX "Sub_created_at_idx" ON "Sub"("created_at");
-- CreateIndex
CREATE INDEX "Sub_userId_idx" ON "Sub"("userId");
-- CreateIndex
CREATE INDEX "Sub_path_idx" ON "Sub" USING GIST ("path" gist_ltree_ops(siglen=2024));
-- AddForeignKey
ALTER TABLE "Sub" ADD CONSTRAINT "Sub_parentName_fkey" FOREIGN KEY ("parentName") REFERENCES "Sub"("name") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Sub" ADD CONSTRAINT "Sub_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SubAct" ADD CONSTRAINT "SubAct_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SubAct" ADD CONSTRAINT "SubAct_subName_fkey" FOREIGN KEY ("subName") REFERENCES "Sub"("name") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE OR REPLACE FUNCTION update_sub_path() RETURNS TRIGGER AS $$
DECLARE
npath ltree;
BEGIN
IF NEW."parentName" IS NULL THEN
SELECT LOWER(NEW.name)::ltree INTO npath;
NEW."path" = npath;
ELSEIF TG_OP = 'INSERT' OR OLD."parentName" IS NULL OR OLD."parentName" != NEW."parentName" THEN
SELECT "path" || LOWER(NEW.name)::text
FROM "Sub"
WHERE name = NEW."parentName"
INTO npath;
IF npath IS NULL THEN
RAISE EXCEPTION 'Invalid parent name %', NEW."parentName";
END IF;
NEW."path" = npath;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER sub_path_tgr
BEFORE INSERT OR UPDATE ON "Sub"
FOR EACH ROW EXECUTE PROCEDURE update_sub_path();
-- fix balance limit check
CREATE OR REPLACE FUNCTION create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone,
msats_req BIGINT, user_id INTEGER, idesc TEXT, comment TEXT, lud18_data JSONB, inv_limit INTEGER, balance_limit_msats BIGINT)
RETURNS "Invoice"
LANGUAGE plpgsql
AS $$
DECLARE
invoice "Invoice";
inv_limit_reached BOOLEAN;
balance_limit_reached BOOLEAN;
inv_pending_msats BIGINT;
BEGIN
PERFORM ASSERT_SERIALIZED();
-- prevent too many pending invoices
SELECT inv_limit > 0 AND count(*) >= inv_limit, COALESCE(sum("msatsRequested"), 0) INTO inv_limit_reached, inv_pending_msats
FROM "Invoice"
WHERE "userId" = user_id AND "expiresAt" > now_utc() AND "confirmedAt" IS NULL AND cancelled = false;
IF inv_limit_reached THEN
RAISE EXCEPTION 'SN_INV_PENDING_LIMIT';
END IF;
-- prevent pending invoices + msats from exceeding the limit
SELECT balance_limit_msats > 0 AND inv_pending_msats+msats_req+msats > balance_limit_msats INTO balance_limit_reached
FROM users
WHERE id = user_id;
IF balance_limit_reached THEN
RAISE EXCEPTION 'SN_INV_EXCEED_BALANCE';
END IF;
-- we good, proceed frens
INSERT INTO "Invoice" (hash, bolt11, "expiresAt", "msatsRequested", "userId", created_at, updated_at, "desc", comment, "lud18Data")
VALUES (hash, bolt11, expires_at, msats_req, user_id, now_utc(), now_utc(), idesc, comment, lud18_data) RETURNING * INTO invoice;
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
VALUES ('checkInvoice', jsonb_build_object('hash', hash), 21, true, now() + interval '10 seconds');
RETURN invoice;
END;
$$;
-- get spenders
DROP MATERIALIZED VIEW IF EXISTS spender_growth_days;
CREATE MATERIALIZED VIEW IF NOT EXISTS spender_growth_days AS
SELECT day, count(DISTINCT "userId") as any,
count(DISTINCT "userId") FILTER (WHERE act = 'STREAM') as jobs,
count(DISTINCT "userId") FILTER (WHERE act = 'BOOST') as boost,
count(DISTINCT "userId") FILTER (WHERE act = 'FEE') as fees,
count(DISTINCT "userId") FILTER (WHERE act = 'TIP') as tips,
count(DISTINCT "userId") FILTER (WHERE act = 'DONATION') as donations,
count(DISTINCT "userId") FILTER (WHERE act = 'TERRITORY') as territories
FROM days
LEFT JOIN
((SELECT "ItemAct".created_at, "userId", act::text as act
FROM "ItemAct")
UNION ALL
(SELECT created_at, "userId", 'DONATION' as act
FROM "Donation")
UNION ALL
(SELECT created_at, "userId", 'TERRITORY' as act
FROM "SubAct"
WHERE type = 'BILLING')
) u ON day = date_trunc('day', timezone('America/Chicago', u.created_at at time zone 'UTC'))
GROUP BY day
ORDER BY day ASC;
-- get spending
DROP MATERIALIZED VIEW IF EXISTS spending_growth_days;
CREATE MATERIALIZED VIEW IF NOT EXISTS spending_growth_days AS
SELECT day, coalesce(floor(sum(msats) FILTER (WHERE act = 'STREAM')/1000), 0) as jobs,
coalesce(floor(sum(msats) FILTER (WHERE act = 'BOOST')/1000), 0) as boost,
coalesce(floor(sum(msats) FILTER (WHERE act NOT IN ('BOOST', 'TIP', 'STREAM', 'DONATION', 'TERRITORY'))/1000), 0) as fees,
coalesce(floor(sum(msats) FILTER (WHERE act = 'TIP')/1000), 0) as tips,
coalesce(floor(sum(msats) FILTER (WHERE act = 'DONATION')/1000), 0) as donations,
coalesce(floor(sum(msats) FILTER (WHERE act = 'TERRITORY')/1000), 0) as territories
FROM days
LEFT JOIN
((SELECT "ItemAct".created_at, msats, act::text as act
FROM "ItemAct")
UNION ALL
(SELECT created_at, sats * 1000 as msats, 'DONATION' as act
FROM "Donation")
UNION ALL
(SELECT created_at, msats, 'TERRITORY' as act
FROM "SubAct"
WHERE type = 'BILLING')
) u ON day = date_trunc('day', timezone('America/Chicago', u.created_at at time zone 'UTC'))
GROUP BY day
ORDER BY day ASC;
-- get stackers
DROP MATERIALIZED VIEW IF EXISTS stackers_growth_days;
CREATE MATERIALIZED VIEW IF NOT EXISTS stackers_growth_days AS
SELECT day, count(distinct user_id) as any,
count(distinct user_id) FILTER (WHERE type = 'POST') as posts,
count(distinct user_id) FILTER (WHERE type = 'COMMENT') as comments,
count(distinct user_id) FILTER (WHERE type = 'EARN') as rewards,
count(distinct user_id) FILTER (WHERE type = 'REFERRAL') as referrals,
count(distinct user_id) FILTER (WHERE type = 'REVENUE') as territories
FROM days
LEFT JOIN
((SELECT "ItemAct".created_at, "Item"."userId" as user_id, CASE WHEN "Item"."parentId" IS NULL THEN 'POST' ELSE 'COMMENT' END as type
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE "ItemAct".act = 'TIP')
UNION ALL
(SELECT created_at, "userId" as user_id, 'EARN' as type
FROM "Earn")
UNION ALL
(SELECT created_at, "referrerId" as user_id, 'REFERRAL' as type
FROM "ReferralAct")
UNION ALL
(SELECT created_at, "userId" as user_id, 'REVENUE' as type
FROM "SubAct"
WHERE type = 'REVENUE')
) u ON day = date_trunc('day', timezone('America/Chicago', u.created_at at time zone 'UTC'))
GROUP BY day
ORDER BY day ASC;
-- get stacking
DROP MATERIALIZED VIEW IF EXISTS stacking_growth_days;
CREATE MATERIALIZED VIEW IF NOT EXISTS stacking_growth_days AS
SELECT day, coalesce(floor(sum(airdrop)/1000),0) as rewards,
coalesce(floor(sum(post)/1000),0) as posts,
coalesce(floor(sum(comment)/1000),0) as comments,
coalesce(floor(sum(referral)/1000),0) as referrals,
coalesce(floor(sum(revenue)/1000),0) as territories
FROM days
LEFT JOIN
((SELECT "ItemAct".created_at, 0 as airdrop,
CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE "ItemAct".msats END as comment,
CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".msats ELSE 0 END as post,
0 as referral, 0 as revenue
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE "ItemAct".act = 'TIP')
UNION ALL
(SELECT created_at, 0 as airdrop, 0 as post, 0 as comment, msats as referral, 0 as revenue
FROM "ReferralAct")
UNION ALL
(SELECT created_at, msats as airdrop, 0 as post, 0 as comment, 0 as referral, 0 as revenue
FROM "Earn")
UNION ALL
(SELECT created_at, 0 as airdrop, 0 as post, 0 as comment, 0 as referral, msats as revenue
FROM "SubAct"
WHERE type = 'REVENUE')
) u ON day = date_trunc('day', timezone('America/Chicago', u.created_at at time zone 'UTC'))
GROUP BY day
ORDER BY day ASC;
-- indices for the other materialized view so we can refresh concurrently
CREATE UNIQUE INDEX IF NOT EXISTS spender_growth_days_idx ON spender_growth_days(day);
CREATE UNIQUE INDEX IF NOT EXISTS spending_growth_days_idx ON spending_growth_days(day);
CREATE UNIQUE INDEX IF NOT EXISTS stackers_growth_days_idx ON stackers_growth_days(day);
CREATE UNIQUE INDEX IF NOT EXISTS stacking_growth_days_idx ON stacking_growth_days(day);