-- 01-partition.sql -- First-run initialization for new-api's partitioned logs table. -- Executed by the postgres official entrypoint against the database named by -- POSTGRES_DB (new-api), as a superuser. -- -- This script is the SINGLE source of truth for the logs partitioning setup. -- The new-api Go application does NOT create or maintain partitions; it only -- skips GORM AutoMigrate for the logs table on PostgreSQL (see -- model/main.go::ensureLogTable). pg_partman owns partition lifecycle, pg_cron -- drives periodic maintenance. -- 1. Extensions -------------------------------------------------------------- -- Install pg_partman into a dedicated schema named "partman" so its functions -- are referenced as partman.xxx (pg_partman 5.x does not create the schema -- automatically; without this CREATE EXTENSION lands in public and -- partman.create_parent fails with "schema partman does not exist"). CREATE SCHEMA IF NOT EXISTS partman; CREATE EXTENSION IF NOT EXISTS pg_partman WITH SCHEMA partman; -- pg_cron must be created in the database set by cron.database_name (new-api), -- which requires shared_preload_libraries='pg_cron' (configured in the image). -- Include partman in the default search_path so its functions resolve without -- schema-qualifying every call below. SET search_path = partman, public; CREATE EXTENSION IF NOT EXISTS pg_cron; -- 2. Partitioned parent table ----------------------------------------------- -- Columns mirror model.Log exactly. PG requires the partition key (created_at) -- to be part of the primary key, so it is a composite (id, created_at). The app -- never queries logs by id alone, so this does not affect business logic. CREATE TABLE IF NOT EXISTS public.logs ( id BIGSERIAL, user_id INTEGER, created_at BIGINT NOT NULL, type INTEGER, content TEXT, username VARCHAR(64) DEFAULT '', token_name VARCHAR(255) DEFAULT '', model_name VARCHAR(255) DEFAULT '', quota INTEGER DEFAULT 0, prompt_tokens INTEGER DEFAULT 0, completion_tokens INTEGER DEFAULT 0, use_time INTEGER DEFAULT 0, is_stream BOOLEAN, channel_id INTEGER, token_id INTEGER DEFAULT 0, "group" VARCHAR(255), ip VARCHAR(64) DEFAULT '', request_id VARCHAR(64) DEFAULT '', chat_id VARCHAR(128) DEFAULT '', upstream_id VARCHAR(128) DEFAULT '', other TEXT, PRIMARY KEY (id, created_at) ) PARTITION BY RANGE (created_at); -- 3. Hand off to pg_partman -------------------------------------------------- -- Weekly native range partitioning on the bigint epoch (seconds) column. -- p_epoch='seconds' -> created_at is a unix-seconds bigint -- p_type='range' -> pg_partman 5.x uses PG-native partitioning with -- p_type values 'range'/'list' (the old 'native' -- alias from 4.x was removed) -- p_interval='1 week' -> one partition per week (pg_partman 5.x dropped -- the 'weekly' preset in favor of native PG -- interval values) -- p_date_trunc_interval='week' -> align partition boundaries to ISO weeks (Monday) -- p_premake=8 -> always keep 8 future weeks pre-created -- p_default_table=true -> create a DEFAULT partition catching out-of-range -- inserts so they never fail silently (RecordConsumeLog -- only logs errors without retrying) -- No retention is set: per current requirement we do NOT auto-drop old partitions. SELECT partman.create_parent( p_parent_table => 'public.logs', p_control => 'created_at', p_type => 'range', p_interval => '1 week', p_epoch => 'seconds', p_date_trunc_interval => 'week', p_premake => 8, p_default_table => true ); -- 4. Core indexes on the parent table --------------------------------------- -- PG 11+ propagates indexes created on a partitioned parent to all child -- partitions automatically. These cover the hot query paths in model/log.go -- (GetAllLogs / GetUserLogs ordering by created_at desc, id desc; lookups by -- user/model/channel/request/token). Trimmed from the model's 16 index tags to -- the 6 actually used, cutting index write overhead. CREATE INDEX IF NOT EXISTS idx_logs_created_at_id ON public.logs (created_at DESC, id DESC); CREATE INDEX IF NOT EXISTS idx_logs_user_id_created ON public.logs (user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_logs_model_name ON public.logs (model_name); CREATE INDEX IF NOT EXISTS idx_logs_channel_id ON public.logs (channel_id); CREATE INDEX IF NOT EXISTS idx_logs_request_id ON public.logs (request_id); CREATE INDEX IF NOT EXISTS idx_logs_token_id ON public.logs (token_id); -- 5. Schedule periodic maintenance ------------------------------------------ -- run_maintenance_proc() inspects partman.part_config and premakes the next -- partitions when needed. Every 30 minutes is more than enough; weekly partitions -- only need creation roughly once a week. No retention => no drops. SELECT cron.schedule( 'log-partition-maint', '*/30 * * * *', $$CALL partman.run_maintenance_proc()$$ );