Files

248 lines
12 KiB
Python

"""Immutable per-language judging configuration and extended submission language IDs.
Revision ID: 0002_multi_runtime
Revises: 0001_judging_foundation
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects.postgresql import JSONB
revision = "0002_multi_runtime"
down_revision = "0001_judging_foundation"
branch_labels = None
depends_on = None
CONFIGURATION_CHECK = """
(judging_schema_version = 1 AND language_configurations IS NULL
AND toolchain IS NOT NULL AND compile_limits IS NOT NULL
AND run_limits IS NOT NULL AND job_wall_time_us IS NOT NULL)
OR
(judging_schema_version = 2 AND language_configurations IS NOT NULL
AND toolchain IS NULL AND compile_limits IS NULL
AND run_limits IS NULL AND job_wall_time_us IS NULL
AND public.kf_valid_language_configurations(languages, language_configurations))
"""
def upgrade() -> None:
# Metadata-only additions/widening; do not UPDATE any immutable published rows.
op.add_column(
"problem_versions",
sa.Column("judging_schema_version", sa.SmallInteger(), nullable=False, server_default="1"),
)
op.add_column(
"problem_versions",
sa.Column("language_configurations", JSONB(none_as_null=True), nullable=True),
)
for column, kind in (
("toolchain", JSONB()),
("compile_limits", JSONB()),
("run_limits", JSONB()),
("job_wall_time_us", sa.BigInteger()),
):
op.alter_column("problem_versions", column, existing_type=kind, nullable=True)
op.alter_column(
"submissions",
"language",
existing_type=sa.String(8),
type_=sa.String(16),
existing_nullable=False,
)
op.drop_constraint(op.f("ck_submissions_language"), "submissions", type_="check")
op.create_check_constraint(
op.f("ck_submissions_language"),
"submissions",
"language IN ('c17','cpp20','python313','java21')",
)
op.execute("""
CREATE FUNCTION public.kf_valid_runtime_limits(limits jsonb)
RETURNS boolean LANGUAGE plpgsql IMMUTABLE STRICT
SET search_path = pg_catalog, public AS $$
DECLARE entry record;
BEGIN
IF jsonb_typeof(limits) != 'object' OR
(SELECT count(*) FROM jsonb_object_keys(limits)) != 10 THEN RETURN false; END IF;
FOR entry IN SELECT * FROM (VALUES
('cpu_time_us',1000::numeric,60000000::numeric),
('wall_time_us',1000,120000000), ('memory_bytes',1,2147483648),
('stack_bytes',1,268435456), ('processes',1,128), ('open_files',3,1024),
('stdout_bytes',1,16777216), ('stderr_bytes',1,16777216),
('file_bytes',1,67108864), ('workdir_bytes',1,268435456)
) AS bounds(name,lo,hi) LOOP
IF NOT limits ? entry.name OR jsonb_typeof(limits->entry.name) != 'number'
OR (limits->>entry.name) !~ '^(0|[1-9][0-9]*)$'
OR length(limits->>entry.name) > 19 THEN RETURN false; END IF;
IF (limits->>entry.name)::numeric NOT BETWEEN entry.lo AND entry.hi
THEN RETURN false; END IF;
END LOOP;
RETURN (limits->>'stack_bytes')::numeric <= (limits->>'memory_bytes')::numeric
AND (limits->>'file_bytes')::numeric <= (limits->>'workdir_bytes')::numeric
AND (limits->>'stdout_bytes')::numeric <= (limits->>'file_bytes')::numeric
AND (limits->>'stderr_bytes')::numeric <= (limits->>'file_bytes')::numeric
AND (limits->>'stdout_bytes')::numeric + (limits->>'stderr_bytes')::numeric
<= (limits->>'workdir_bytes')::numeric;
END $$
""")
op.execute("""
CREATE FUNCTION public.kf_valid_language_configurations(languages jsonb, configs jsonb)
RETURNS boolean LANGUAGE plpgsql IMMUTABLE STRICT
SET search_path = pg_catalog, public AS $$
DECLARE item record; cfg jsonb; runtime jsonb; lang text; seen text[] := ARRAY[]::text[];
phase jsonb; preparing boolean;
BEGIN
IF jsonb_typeof(languages) != 'array' OR jsonb_typeof(configs) != 'array'
THEN RETURN false; END IF;
IF jsonb_array_length(configs) NOT BETWEEN 1 AND 4 OR
jsonb_array_length(languages) != jsonb_array_length(configs)
THEN RETURN false; END IF;
FOR item IN SELECT value, ordinality FROM jsonb_array_elements(configs)
WITH ORDINALITY LOOP
cfg := item.value;
IF jsonb_typeof(cfg) != 'object' OR
(SELECT count(*) FROM jsonb_object_keys(cfg)) != 5 OR NOT cfg ?&
ARRAY['language','toolchain','compile_limits','run_limits','job_wall_time_us']
THEN RETURN false; END IF;
IF jsonb_typeof(cfg->'language') != 'string' THEN RETURN false; END IF;
lang := cfg->>'language';
IF lang NOT IN ('c17','cpp20','python313','java21') OR lang = ANY(seen)
OR languages->(item.ordinality::int-1) != cfg->'language'
THEN RETURN false; END IF;
seen := array_append(seen,lang); runtime := cfg->'toolchain';
IF jsonb_typeof(runtime) != 'object' OR
(SELECT count(*) FROM jsonb_object_keys(runtime)) != 5 OR NOT runtime ?&
ARRAY['toolchain_id','image_kind','image_revision','image_digest','runtime_version']
THEN RETURN false; END IF;
IF jsonb_typeof(runtime->'toolchain_id') != 'string' OR
jsonb_typeof(runtime->'image_kind') != 'string' OR
jsonb_typeof(runtime->'runtime_version') != 'string' OR
jsonb_typeof(runtime->'image_digest') != 'string' OR
(runtime->>'image_digest') !~ '^[0-9a-f]{64}$' OR
(runtime->>'runtime_version') !~ '^[A-Za-z0-9][A-Za-z0-9_.+-]{0,63}$' OR
jsonb_typeof(runtime->'image_revision') != 'number' OR
(runtime->>'image_revision') !~ '^[1-9][0-9]{0,9}$'
THEN RETURN false; END IF;
IF (runtime->>'image_revision')::numeric > 4294967295 THEN RETURN false; END IF;
IF lang IN ('c17','cpp20') AND (runtime->>'toolchain_id' != 'gnu-cxx-v1' OR
runtime->>'image_kind' != 'linux_gnu_cxx_x86_64_v1') THEN RETURN false; END IF;
IF lang = 'python313' AND (runtime->>'toolchain_id' != 'cpython313-v1' OR
runtime->>'image_kind' != 'linux_cpython313_x86_64_v1' OR
runtime->>'runtime_version' != '3.13.16') THEN RETURN false; END IF;
IF lang = 'java21' AND (runtime->>'toolchain_id' != 'openjdk21-v1' OR
runtime->>'image_kind' != 'linux_openjdk21_x86_64_v1' OR
(runtime->>'runtime_version') !~
'^21[.][0-9]+[.][0-9]+([.][0-9]+)?[+][1-9][0-9]*(-LTS)?$')
THEN RETURN false; END IF;
IF NOT public.kf_valid_runtime_limits(cfg->'compile_limits') OR
NOT public.kf_valid_runtime_limits(cfg->'run_limits') THEN RETURN false; END IF;
IF jsonb_typeof(cfg->'job_wall_time_us') != 'number' OR
(cfg->>'job_wall_time_us') !~ '^[1-9][0-9]{0,9}$' THEN RETURN false; END IF;
IF (cfg->>'job_wall_time_us')::numeric NOT BETWEEN 1000 AND 3600000000 OR
(cfg->'compile_limits'->>'wall_time_us')::numeric +
(cfg->'run_limits'->>'wall_time_us')::numeric >
(cfg->>'job_wall_time_us')::numeric
THEN RETURN false; END IF;
FOR preparing, phase IN SELECT true,cfg->'compile_limits' UNION ALL
SELECT false,cfg->'run_limits' LOOP
IF lang = 'python313' AND ((phase->>'cpu_time_us')::numeric < 10000 OR
(phase->>'wall_time_us')::numeric < 50000 OR
(phase->>'memory_bytes')::numeric < 33554432 OR
(phase->>'open_files')::numeric < 16) THEN RETURN false; END IF;
IF lang = 'java21' AND ((phase->>'cpu_time_us')::numeric <
CASE WHEN preparing THEN 1000000 ELSE 200000 END OR
(phase->>'wall_time_us')::numeric <
CASE WHEN preparing THEN 3000000 ELSE 1000000 END OR
(phase->>'memory_bytes')::numeric <
CASE WHEN preparing THEN 402653184 ELSE 268435456 END OR
(phase->>'processes')::numeric < 16 OR
(phase->>'open_files')::numeric < 32 OR
(phase->>'stack_bytes')::numeric < 262144 OR
(phase->>'memory_bytes')::numeric - 167772160 -
(phase->>'processes')::numeric * 262144 < 33554432)
THEN RETURN false; END IF;
END LOOP;
END LOOP;
RETURN true;
END $$
""")
op.execute("REVOKE ALL ON FUNCTION public.kf_valid_runtime_limits(jsonb) FROM PUBLIC")
op.execute(
"REVOKE ALL ON FUNCTION public.kf_valid_language_configurations(jsonb,jsonb) FROM PUBLIC"
)
op.execute("GRANT EXECUTE ON FUNCTION public.kf_valid_runtime_limits(jsonb) TO kf_oj_platform")
op.execute(
"GRANT EXECUTE ON FUNCTION public.kf_valid_language_configurations(jsonb,jsonb) "
"TO kf_oj_platform"
)
op.create_check_constraint(
op.f("ck_problem_versions_judging_schema"),
"problem_versions",
"judging_schema_version IN (1,2)",
)
op.create_check_constraint(
op.f("ck_problem_versions_language_configurations"), "problem_versions", CONFIGURATION_CHECK
)
op.execute("""
CREATE FUNCTION public.kf_submission_language() RETURNS trigger LANGUAGE plpgsql
SET search_path = pg_catalog, public AS $$
DECLARE supported jsonb;
BEGIN
SELECT languages INTO supported FROM public.problem_versions
WHERE id=NEW.problem_version_id;
IF FOUND AND NOT supported @> jsonb_build_array(NEW.language) THEN
RAISE EXCEPTION 'submission language not supported by version'
USING ERRCODE = '23514';
END IF;
RETURN NEW;
END $$
""")
op.execute("REVOKE ALL ON FUNCTION public.kf_submission_language() FROM PUBLIC")
op.execute("""
CREATE TRIGGER kf_submission_language BEFORE INSERT OR UPDATE OF
language,problem_version_id ON public.submissions
FOR EACH ROW EXECUTE FUNCTION public.kf_submission_language()
""")
def downgrade() -> None:
# Losing a runtime configuration is not a rollback strategy. Preserve all user data.
op.execute("""
DO $$ BEGIN
IF EXISTS (SELECT 1 FROM problem_versions WHERE judging_schema_version = 2) OR
EXISTS (SELECT 1 FROM submissions WHERE language NOT IN ('c17','cpp20')) THEN
RAISE EXCEPTION 'cannot downgrade while multi-runtime data exists'
USING ERRCODE = '23514';
END IF;
END $$
""")
op.drop_constraint(
op.f("ck_problem_versions_language_configurations"), "problem_versions", type_="check"
)
op.drop_constraint(
op.f("ck_problem_versions_judging_schema"), "problem_versions", type_="check"
)
op.execute("DROP TRIGGER kf_submission_language ON public.submissions")
op.execute("DROP FUNCTION public.kf_submission_language()")
op.execute("DROP FUNCTION public.kf_valid_language_configurations(jsonb,jsonb)")
op.execute("DROP FUNCTION public.kf_valid_runtime_limits(jsonb)")
op.drop_column("problem_versions", "language_configurations")
op.drop_column("problem_versions", "judging_schema_version")
for column, kind in (
("toolchain", JSONB()),
("compile_limits", JSONB()),
("run_limits", JSONB()),
("job_wall_time_us", sa.BigInteger()),
):
op.alter_column("problem_versions", column, existing_type=kind, nullable=False)
op.drop_constraint(op.f("ck_submissions_language"), "submissions", type_="check")
op.create_check_constraint(
op.f("ck_submissions_language"), "submissions", "language IN ('c17','cpp20')"
)
op.alter_column(
"submissions",
"language",
existing_type=sa.String(16),
type_=sa.String(8),
existing_nullable=False,
)