248 lines
12 KiB
Python
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,
|
|
)
|