From 284d7f3048e7fec443f8b3402f636d49886fe2a4 Mon Sep 17 00:00:00 2001 From: Aida Paul Date: Tue, 26 May 2026 10:32:17 +0200 Subject: [PATCH 1/3] PLATL-650: add soda_runner alongside soda_agent in lint contract schema (backwards-compatible) Mirrors the agent->runner rename landing in soda-server, soda-webapp, soda-core, and soda-extensions. The lint command validates contracts against this embedded JSON Schema; without the change, contracts using the new `soda_runner` key are rejected because the root has `additionalProperties: false`. - Adds `soda_runner` as a new top-level property (same shape as `soda_agent`: `checks_schedule` with cron / timezone / variables). - Rewrites `soda_agent` description to "DEPRECATED: use 'soda_runner' instead. Kept for backwards compatibility." - Adds a root `oneOf` enforcing "exactly one of the two keys, or neither" so contracts that set both fail validation cleanly. No Go code changes -- the schema is consumed via `santhosh-tekuri/jsonschema/v6` as untyped JSON Schema, so there is no key-name lookup to update. Filename is unchanged (`soda_data_contract_json_schema_1_0_0.json`) so the `//go:embed` directive in `schema.go` keeps working; this matches the filename-stability decision in soda-core#2718. Companion PR landing the same rename across the Java backend, the Angular contract editor, and tests: https://github.com/sodadata/soda/pull/12250 Linear: PLATL-650 https://linear.app/sodadata/issue/PLATL-650 --- .../soda_data_contract_json_schema_1_0_0.json | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/go/internal/lint/schema/soda_data_contract_json_schema_1_0_0.json b/go/internal/lint/schema/soda_data_contract_json_schema_1_0_0.json index 89a360c..300704e 100644 --- a/go/internal/lint/schema/soda_data_contract_json_schema_1_0_0.json +++ b/go/internal/lint/schema/soda_data_contract_json_schema_1_0_0.json @@ -38,7 +38,37 @@ } }, "soda_agent": { - "description": "The agent configuration to be used within the contract file", + "description": "DEPRECATED: use 'soda_runner' instead. The runner configuration to be used within the contract file. Kept for backwards compatibility.", + "type": "object", + "properties": { + "checks_schedule": { + "type": "object", + "properties": { + "cron": { + "type": "string", + "description": "Cron expression in the format '* * * * *' (minute hour day month weekday). Requires string in quotes." + }, + "timezone": { + "type": "string", + "description": "The timezone to be used within the contract file" + }, + "variables": { + "type": "object", + "description": "The variables to be used within the contract file", + "additionalProperties": { + "type": [ + "string", + "number" + ], + "additionalProperties": false + } + } + } + } + } + }, + "soda_runner": { + "description": "The runner configuration to be used within the contract file", "type": "object", "properties": { "checks_schedule": { @@ -837,6 +867,16 @@ "dataset", "columns" ], + "oneOf": [ + { "required": ["soda_runner"] }, + { "required": ["soda_agent"] }, + { + "allOf": [ + { "not": { "required": ["soda_runner"] } }, + { "not": { "required": ["soda_agent"] } } + ] + } + ], "$defs": { "generic_check_properties": { "description": "NOT USED YET, problem with inheritance and additionalProperties", From 92ed81e76628069180a4b99aea7cbc1d7c6db1c8 Mon Sep 17 00:00:00 2001 From: Aida Paul Date: Tue, 26 May 2026 11:15:57 +0200 Subject: [PATCH 2/3] review: simplify oneOf, drop dead additionalProperties, adopt soda-core description, add lint tests Addresses review feedback on PR 14: - Schema: replace 3-branch oneOf with {not: {required: [soda_runner, soda_agent]}}. Logically equivalent; produces a cleaner validator error (`$: 'not' failed` vs `must be valid to exactly one schema in oneOf`). - Schema: drop dead `additionalProperties: false` nested inside the variables value schema. The keyword is unreachable because the value's `type` is ["string", "number"] (never an object). Pre-existing leftover, copied into the new soda_runner block; fixed in both. - Schema: align soda_runner description with soda-core PR #2718 wording for cross-stack consistency. Tests (lint_test.go): - TestLintFile_AcceptsSodaRunner - TestLintFile_AcceptsSodaAgentLegacyAlias - TestLintFile_RejectsBothSodaRunnerAndSodaAgent All 9 lint tests pass. --- go/internal/lint/lint_test.go | 80 +++++++++++++++++++ .../soda_data_contract_json_schema_1_0_0.json | 21 ++--- 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/go/internal/lint/lint_test.go b/go/internal/lint/lint_test.go index 8b7219b..a8278b8 100644 --- a/go/internal/lint/lint_test.go +++ b/go/internal/lint/lint_test.go @@ -148,6 +148,86 @@ unknown_prop: true } } +func TestLintFile_AcceptsSodaRunner(t *testing.T) { + dir := t.TempDir() + f := filepath.Join(dir, "contract.yml") + os.WriteFile(f, []byte(` +dataset: my_ds/db/schema/orders +columns: + - name: id +soda_runner: + checks_schedule: + cron: "0 0 * * *" + timezone: UTC +`), 0644) + + result, err := LintFile(f) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !result.Valid { + for _, e := range result.Errors { + t.Logf(" %s: %s", e.Path, e.Message) + } + t.Fatalf("expected valid contract with soda_runner, got %d errors", len(result.Errors)) + } +} + +func TestLintFile_AcceptsSodaAgentLegacyAlias(t *testing.T) { + dir := t.TempDir() + f := filepath.Join(dir, "contract.yml") + os.WriteFile(f, []byte(` +dataset: my_ds/db/schema/orders +columns: + - name: id +soda_agent: + checks_schedule: + cron: "0 0 * * *" + timezone: UTC +`), 0644) + + result, err := LintFile(f) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !result.Valid { + for _, e := range result.Errors { + t.Logf(" %s: %s", e.Path, e.Message) + } + t.Fatalf("expected valid contract with deprecated soda_agent, got %d errors", len(result.Errors)) + } +} + +func TestLintFile_RejectsBothSodaRunnerAndSodaAgent(t *testing.T) { + dir := t.TempDir() + f := filepath.Join(dir, "contract.yml") + os.WriteFile(f, []byte(` +dataset: my_ds/db/schema/orders +columns: + - name: id +soda_runner: + checks_schedule: + cron: "0 0 * * *" +soda_agent: + checks_schedule: + cron: "0 0 * * *" +`), 0644) + + result, err := LintFile(f) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result.Valid { + t.Fatal("expected invalid when both soda_runner and soda_agent are set") + } + if len(result.Errors) == 0 { + t.Fatal("expected at least one validation error") + } + for _, e := range result.Errors { + t.Logf(" %s: %s", e.Path, e.Message) + } +} + func TestSegmentsToPath(t *testing.T) { tests := []struct { in []string diff --git a/go/internal/lint/schema/soda_data_contract_json_schema_1_0_0.json b/go/internal/lint/schema/soda_data_contract_json_schema_1_0_0.json index 300704e..d78d3bf 100644 --- a/go/internal/lint/schema/soda_data_contract_json_schema_1_0_0.json +++ b/go/internal/lint/schema/soda_data_contract_json_schema_1_0_0.json @@ -59,8 +59,7 @@ "type": [ "string", "number" - ], - "additionalProperties": false + ] } } } @@ -68,7 +67,7 @@ } }, "soda_runner": { - "description": "The runner configuration to be used within the contract file", + "description": "The runner configuration to be used within the contract file. Replaces 'soda_agent'; both keys are accepted for backwards compatibility but not at the same time.", "type": "object", "properties": { "checks_schedule": { @@ -89,8 +88,7 @@ "type": [ "string", "number" - ], - "additionalProperties": false + ] } } } @@ -867,16 +865,9 @@ "dataset", "columns" ], - "oneOf": [ - { "required": ["soda_runner"] }, - { "required": ["soda_agent"] }, - { - "allOf": [ - { "not": { "required": ["soda_runner"] } }, - { "not": { "required": ["soda_agent"] } } - ] - } - ], + "not": { + "required": ["soda_runner", "soda_agent"] + }, "$defs": { "generic_check_properties": { "description": "NOT USED YET, problem with inheritance and additionalProperties", From d310fc6377327aa368cfc9dd85314f502b101cfb Mon Sep 17 00:00:00 2001 From: Aida Paul Date: Tue, 26 May 2026 11:31:54 +0200 Subject: [PATCH 3/3] review pass 2: tighten RejectsBoth test to assert error path is root Per second-pass review feedback, the previous version of TestLintFile_RejectsBothSodaRunnerAndSodaAgent asserted only that the contract was rejected with at least one error -- not that the error surfaced at the document root ('$') where the new `not: {required: [soda_runner, soda_agent]}` constraint produces it. Adds an assertion that one of the reported errors has Path == "$" so the user-visible error location doesn't silently regress to e.g. a property-level report. --- go/internal/lint/lint_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/go/internal/lint/lint_test.go b/go/internal/lint/lint_test.go index a8278b8..9862e9d 100644 --- a/go/internal/lint/lint_test.go +++ b/go/internal/lint/lint_test.go @@ -223,8 +223,15 @@ soda_agent: if len(result.Errors) == 0 { t.Fatal("expected at least one validation error") } + rootError := false for _, e := range result.Errors { t.Logf(" %s: %s", e.Path, e.Message) + if e.Path == "$" { + rootError = true + } + } + if !rootError { + t.Fatal("expected a validation error at the root path '$' for the not-both constraint") } }