This is an automated email from the ASF dual-hosted git repository.
laskoviymishka pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-verification.git
The following commit(s) were added to refs/heads/main by this push:
new 004e6ed Add type conformance fixtures and validation (#9)
004e6ed is described below
commit 004e6edde4c2e4276798ffccffb3cd6b8768be4b
Author: Neelesh Salian <[email protected]>
AuthorDate: Wed Sep 30 08:13:57 2026 -0700
Add type conformance fixtures and validation (#9)
Add type conformance fixtures and validation
spaced decimal canonical, skip state, should-tier geo defaults,
decoded-forbid schema, self-test, check-license quoting
---
.github/workflows/license_check.yml | 41 +++++++++++
.github/workflows/validate-fixtures.yml | 70 +++++++++++++++++++
.gitignore | 7 ++
dev/.rat-excludes | 26 +++++++
dev/check-license | 78 +++++++++++++++++++++
dev/requirements.txt | 19 +++++
dev/schema/cases.base.schema.json | 49 +++++++++++++
dev/schema/cases.types.schema.json | 77 ++++++++++++++++++++
dev/validate-fixtures.py | 107 ++++++++++++++++++++++++++++
table-spec/types/README.md | 119 +++++++++++++++++++++++++++++++
table-spec/types/geospatial/cases.json | 120 ++++++++++++++++++++++++++++++++
table-spec/types/nested/cases.json | 96 +++++++++++++++++++++++++
table-spec/types/primitive/cases.json | 34 +++++++++
table-spec/types/variant/cases.json | 6 ++
14 files changed, 849 insertions(+)
diff --git a/.github/workflows/license_check.yml
b/.github/workflows/license_check.yml
new file mode 100644
index 0000000..ee54cb4
--- /dev/null
+++ b/.github/workflows/license_check.yml
@@ -0,0 +1,41 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: "Run License Check"
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+concurrency:
+ group: ${{ github.repository }}-${{ github.head_ref || github.sha }}-${{
github.workflow }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+permissions:
+ contents: read
+
+jobs:
+ rat:
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - run: |
+ dev/check-license
diff --git a/.github/workflows/validate-fixtures.yml
b/.github/workflows/validate-fixtures.yml
new file mode 100644
index 0000000..cc78359
--- /dev/null
+++ b/.github/workflows/validate-fixtures.yml
@@ -0,0 +1,70 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: "Validate Fixtures"
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+concurrency:
+ group: ${{ github.repository }}-${{ github.head_ref || github.sha }}-${{
github.workflow }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+permissions:
+ contents: read
+
+jobs:
+ validate:
+ name: Validate fixture cases
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - name: Install Python
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 #
v6.3.0
+ with:
+ python-version: '3.12'
+ - name: Install validator deps
+ run: python3 -m pip install -r dev/requirements.txt
+ - name: Validate cases.json
+ run: python3 dev/validate-fixtures.py
+ - name: Validator rejects malformed fixtures (self-test)
+ run: |
+ set -e
+ assert_reject() { # <desc> <cases-json>
+ d="$(mktemp -d)"
+ mkdir -p "$d/table-spec/types/x"
+ printf '%s' "$2" > "$d/table-spec/types/x/cases.json"
+ test -f "$d/table-spec/types/x/cases.json"
+ if python3 dev/validate-fixtures.py "$d" >/dev/null 2>&1; then
+ echo "self-test FAILED: validator accepted a malformed fixture
($1)"; exit 1
+ fi
+ echo "self-test ok: rejected ($1)"
+ rm -rf "$d"
+ }
+ assert_reject "valid:true with no decoded"
'{"cases":[{"id":"a","valid":true,"input":"int","clause":"c","spec_ref":"s"}]}'
+ assert_reject "valid:false carrying decoded"
'{"cases":[{"id":"a","valid":false,"input":"bad","decoded":{"type":"int"},"clause":"c","spec_ref":"s"}]}'
+ assert_reject "unknown property"
'{"cases":[{"id":"a","valid":true,"input":"int","decoded":{"type":"int"},"clause":"c","spec_ref":"s","spec-ref":"typo"}]}'
+ assert_reject "duplicate id in a surface"
'{"cases":[{"id":"a","valid":true,"input":"int","decoded":{"type":"int"},"clause":"c","spec_ref":"s"},{"id":"a","valid":true,"input":"long","decoded":{"type":"long"},"clause":"c","spec_ref":"s"}]}'
+ assert_reject "decoded decimal typo (precison)"
'{"cases":[{"id":"a","valid":true,"input":"decimal(9,2)","decoded":{"type":"decimal","precison":9,"scale":2},"clause":"c","spec_ref":"s"}]}'
+ assert_reject "decoded fixed carrying len not length"
'{"cases":[{"id":"a","valid":true,"input":"fixed[16]","decoded":{"type":"fixed","len":16},"clause":"c","spec_ref":"s"}]}'
+ assert_reject "decoded unknown type name"
'{"cases":[{"id":"a","valid":true,"input":"x","decoded":{"type":"notatype"},"clause":"c","spec_ref":"s"}]}'
+ assert_reject "decoded simple type with extra key"
'{"cases":[{"id":"a","valid":true,"input":"int","decoded":{"type":"int","precision":9},"clause":"c","spec_ref":"s"}]}'
diff --git a/.gitignore b/.gitignore
index 9d61ad5..b8ac23d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,10 @@
*~
.idea/
.vscode/
+
+# Apache RAT jar downloaded by dev/check-license
+/lib/
+
+# Python
+__pycache__/
+*.py[cod]
diff --git a/dev/.rat-excludes b/dev/.rat-excludes
new file mode 100644
index 0000000..bc614e9
--- /dev/null
+++ b/dev/.rat-excludes
@@ -0,0 +1,26 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+.gitignore
+LICENSE
+NOTICE
+lib
+
+# Fixture data and JSON Schemas have no comment syntax, so they cannot carry an
+# Apache header; they are covered by the repository LICENSE and NOTICE at the
root.
+**/*.json
+**/*.jsonl
diff --git a/dev/check-license b/dev/check-license
new file mode 100755
index 0000000..be3aead
--- /dev/null
+++ b/dev/check-license
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+acquire_rat_jar () {
+
+
URL="https://repo.maven.apache.org/maven2/org/apache/rat/apache-rat/${RAT_VERSION}/apache-rat-${RAT_VERSION}.jar"
+
+ JAR="$rat_jar"
+
+ # Download rat launch jar if it hasn't been downloaded yet
+ if [ ! -f "$JAR" ]; then
+ # Download
+ printf "Attempting to fetch rat\n"
+ JAR_DL="${JAR}.part"
+ if [ $(command -v curl) ]; then
+ curl -L --silent "${URL}" > "$JAR_DL" && mv "$JAR_DL" "$JAR"
+ elif [ $(command -v wget) ]; then
+ wget --quiet "${URL}" -O "$JAR_DL" && mv "$JAR_DL" "$JAR"
+ else
+ printf "You do not have curl or wget installed, please install rat
manually.\n"
+ return 1
+ fi
+ fi
+
+ unzip -tq "$JAR" &> /dev/null
+ if [ $? -ne 0 ]; then
+ # We failed to download
+ rm "$JAR"
+ printf "Our attempt to download rat locally to ${JAR} failed. Please
install rat manually.\n"
+ return 1
+ fi
+}
+
+# Go to the project root directory
+FWDIR="$(cd "`dirname "$0"`"/..; pwd)"
+cd "$FWDIR"
+
+if test -x "$JAVA_HOME/bin/java"; then
+ declare java_cmd="$JAVA_HOME/bin/java"
+else
+ declare java_cmd=java
+fi
+
+export RAT_VERSION=0.17
+export rat_jar="$FWDIR/lib/apache-rat-${RAT_VERSION}.jar"
+mkdir -p "$FWDIR/lib"
+
+[[ -f "$rat_jar" ]] || acquire_rat_jar || {
+ echo "Download failed. Obtain the rat jar manually and place it at
$rat_jar"
+ exit 1
+}
+
+# --input-exclude-std GIT skips .git and anything .gitignore'd. Without it RAT
+# flags the .git pointer *file* that git worktrees use, since its built-in SCM
+# exclusion only covers .git directories.
+"$java_cmd" -jar "$rat_jar" \
+ --input-exclude-file "$FWDIR/dev/.rat-excludes" \
+ --input-exclude-std GIT IDEA MAC \
+ --input-include-std HIDDEN_DIR \
+ --output-style missing-headers \
+ --log-level ERROR \
+ -- "$FWDIR" || exit 1
+
+echo "RAT checks passed."
diff --git a/dev/requirements.txt b/dev/requirements.txt
new file mode 100644
index 0000000..29c27ff
--- /dev/null
+++ b/dev/requirements.txt
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Dependencies for the dev/ tooling (fixture validation).
+jsonschema>=4.18,<5
diff --git a/dev/schema/cases.base.schema.json
b/dev/schema/cases.base.schema.json
new file mode 100644
index 0000000..d184879
--- /dev/null
+++ b/dev/schema/cases.base.schema.json
@@ -0,0 +1,49 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://iceberg.apache.org/verification/cases.base.schema.json",
+ "title": "Conformance cases (base)",
+ "description": "Structure shared by every table-spec surface's cases.json.",
+ "type": "object",
+ "required": ["cases"],
+ "additionalProperties": false,
+ "properties": {
+ "cases": {
+ "type": "array",
+ "items": { "$ref": "#/$defs/case" }
+ }
+ },
+ "$defs": {
+ "case": {
+ "type": "object",
+ "required": ["id", "valid", "input"],
+ "additionalProperties": false,
+ "properties": {
+ "id": { "type": "string", "minLength": 1 },
+ "valid": { "type": "boolean" },
+ "input": true,
+ "decoded": true,
+ "canonical": { "type": "string" },
+ "clause": { "type": "string" },
+ "spec_ref": { "type": "string" },
+ "normative_level": { "enum": ["must", "should"], "description": "must
(default) or should. A should case is advisory (the spec only recommends the
behavior); a runner must report a failed should distinctly from a pass, so the
advisory tier stays visible rather than reading as inert." }
+ },
+ "allOf": [
+ {
+ "$comment": "a valid case must carry decoded",
+ "if": { "properties": { "valid": { "const": true } }, "required":
["valid"] },
+ "then": { "required": ["decoded"] }
+ },
+ {
+ "$comment": "an invalid case must not carry decoded",
+ "if": { "properties": { "valid": { "const": false } }, "required":
["valid"] },
+ "then": { "properties": { "decoded": false } }
+ },
+ {
+ "$comment": "canonical is only allowed on a valid case",
+ "if": { "required": ["canonical"] },
+ "then": { "properties": { "valid": { "const": true } }, "required":
["valid"] }
+ }
+ ]
+ }
+ }
+}
diff --git a/dev/schema/cases.types.schema.json
b/dev/schema/cases.types.schema.json
new file mode 100644
index 0000000..03c179a
--- /dev/null
+++ b/dev/schema/cases.types.schema.json
@@ -0,0 +1,77 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://iceberg.apache.org/verification/cases.types.schema.json",
+ "title": "Conformance cases (types surface extension)",
+ "description": "Extra constraints for table-spec/types: every case cites the
spec, and a case's decoded matches the per-type shape from Appendix C. Applied
on top of cases.base.schema.json by dev/validate-fixtures.py.",
+ "type": "object",
+ "required": ["cases"],
+ "properties": {
+ "cases": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": ["clause", "spec_ref"],
+ "properties": {
+ "clause": { "type": "string", "minLength": 1 },
+ "spec_ref": { "type": "string", "minLength": 1 },
+ "decoded": { "$ref": "#/$defs/decodedType" }
+ }
+ }
+ }
+ },
+ "$defs": {
+ "decodedType": {
+ "type": "object",
+ "required": ["type"],
+ "properties": {
+ "type": { "enum": ["boolean", "int", "long", "float", "double",
"date", "time", "timestamp", "timestamptz", "timestamp_ns", "timestamptz_ns",
"string", "uuid", "binary", "unknown", "variant", "decimal", "fixed",
"geometry", "geography", "struct", "list", "map"] }
+ },
+ "allOf": [
+ {
+ "$comment": "simple types carry only type",
+ "if": { "required": ["type"], "properties": { "type": { "enum":
["boolean", "int", "long", "float", "double", "date", "time", "timestamp",
"timestamptz", "timestamp_ns", "timestamptz_ns", "string", "uuid", "binary",
"unknown", "variant"] } } },
+ "then": { "properties": { "type": {} }, "additionalProperties":
false }
+ },
+ {
+ "if": { "required": ["type"], "properties": { "type": { "const":
"decimal" } } },
+ "then": { "required": ["precision", "scale"], "properties": {
"type": {}, "precision": { "type": "integer" }, "scale": { "type": "integer" }
}, "additionalProperties": false }
+ },
+ {
+ "if": { "required": ["type"], "properties": { "type": { "const":
"fixed" } } },
+ "then": { "required": ["length"], "properties": { "type": {},
"length": { "type": "integer" } }, "additionalProperties": false }
+ },
+ {
+ "if": { "required": ["type"], "properties": { "type": { "const":
"geometry" } } },
+ "then": { "required": ["crs"], "properties": { "type": {}, "crs": {
"type": "string" } }, "additionalProperties": false }
+ },
+ {
+ "if": { "required": ["type"], "properties": { "type": { "const":
"geography" } } },
+ "then": { "required": ["crs", "algorithm"], "properties": { "type":
{}, "crs": { "type": "string" }, "algorithm": { "type": "string" } },
"additionalProperties": false }
+ },
+ {
+ "if": { "required": ["type"], "properties": { "type": { "const":
"struct" } } },
+ "then": { "required": ["fields"], "properties": { "type": {},
"fields": { "type": "array", "items": { "$ref": "#/$defs/field" } } },
"additionalProperties": false }
+ },
+ {
+ "if": { "required": ["type"], "properties": { "type": { "const":
"list" } } },
+ "then": { "required": ["element-id", "element-required", "element"],
"properties": { "type": {}, "element-id": { "type": "integer" },
"element-required": { "type": "boolean" }, "element": { "$ref":
"#/$defs/decodedType" } }, "additionalProperties": false }
+ },
+ {
+ "if": { "required": ["type"], "properties": { "type": { "const":
"map" } } },
+ "then": { "required": ["key-id", "key", "value-id",
"value-required", "value"], "properties": { "type": {}, "key-id": { "type":
"integer" }, "key": { "$ref": "#/$defs/decodedType" }, "value-id": { "type":
"integer" }, "value-required": { "type": "boolean" }, "value": { "$ref":
"#/$defs/decodedType" } }, "additionalProperties": false }
+ }
+ ]
+ },
+ "field": {
+ "type": "object",
+ "required": ["id", "name", "required", "type"],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "required": { "type": "boolean" },
+ "type": { "$ref": "#/$defs/decodedType" }
+ },
+ "additionalProperties": false
+ }
+ }
+}
diff --git a/dev/validate-fixtures.py b/dev/validate-fixtures.py
new file mode 100755
index 0000000..97ebb40
--- /dev/null
+++ b/dev/validate-fixtures.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Validate the conformance type fixtures against the JSON Schemas in
dev/schema/.
+
+Each cases.json is validated against the base structural schema and the types
schema
+(which requires clause and spec_ref); case ids must be unique within a surface.
+"""
+
+import glob
+import json
+import os
+import sys
+
+import jsonschema
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+SCHEMA_DIR = os.path.join(HERE, "schema")
+
+
+def _load_schema(name):
+ with open(os.path.join(SCHEMA_DIR, name), encoding="utf-8") as fh:
+ return json.load(fh)
+
+
+def _rel(path):
+ return path.replace(os.sep, "/")
+
+
+def _is_types_surface(path):
+ p = _rel(path)
+ return "/table-spec/types/" in p or p.startswith("table-spec/types/")
+
+
+def main():
+ root = sys.argv[1] if len(sys.argv) > 1 else "."
+ base_validator =
jsonschema.Draft202012Validator(_load_schema("cases.base.schema.json"))
+ types_validator =
jsonschema.Draft202012Validator(_load_schema("cases.types.schema.json"))
+
+ files = sorted(glob.glob(f"{root}/table-spec/**/cases.json",
recursive=True))
+ if not files:
+ print("no cases.json files found", file=sys.stderr)
+ return 1
+
+ errors = []
+ seen_ids = {}
+ total = 0
+ for path in files:
+ try:
+ with open(path, encoding="utf-8") as fh:
+ doc = json.load(fh)
+ except json.JSONDecodeError as e:
+ errors.append(f"{path}: invalid JSON: {e}")
+ continue
+
+ validators = [base_validator]
+ if _is_types_surface(path):
+ validators.append(types_validator)
+ for validator in validators:
+ for err in sorted(validator.iter_errors(doc), key=str):
+ where = "/".join(str(p) for p in err.absolute_path) or "(root)"
+ errors.append(f"{path}: {where}: {err.message}")
+
+ cases = doc.get("cases") if isinstance(doc, dict) else None
+ if isinstance(cases, list):
+ # ids are unique per surface (the top-level dir under table-spec),
so a
+ # future surface may reuse names like int/string/timestamp.
+ parts = _rel(os.path.relpath(path, root)).split("/")
+ surface = parts[parts.index("table-spec") + 1] if "table-spec" in
parts and parts.index("table-spec") + 1 < len(parts) else parts[0]
+ for i, case in enumerate(cases):
+ if isinstance(case, dict) and isinstance(case.get("id"), str)
and case["id"]:
+ cid = case["id"]
+ key = (surface, cid)
+ if key in seen_ids:
+ errors.append(f"{path}[{i}]: duplicate id '{cid}' in
surface '{surface}' (first seen at {seen_ids[key]})")
+ else:
+ seen_ids[key] = f"{path}[{i}]"
+ total += len(cases)
+ print(f" {path}: {len(cases)} cases")
+
+ if errors:
+ print(f"\nFAILED: {len(errors)} problem(s) in {len(files)} file(s):",
file=sys.stderr)
+ for e in errors:
+ print(f" {e}", file=sys.stderr)
+ return 1
+
+ print(f"\nOK: {total} cases across {len(files)} file(s), {len(seen_ids)}
unique ids.")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/table-spec/types/README.md b/table-spec/types/README.md
new file mode 100644
index 0000000..7b4cf6e
--- /dev/null
+++ b/table-spec/types/README.md
@@ -0,0 +1,119 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ -->
+
+# Type decoding
+
+Parsing a type string produces the same type in every implementation. This
+surface pins each type the spec defines and the parse rules that attach to it.
+
+## Assertion
+
+```
+parse(input) == decoded
+```
+
+`input` is a type string, or a JSON object for a nested type; `decoded` is the
+language-neutral shape below. Bytes are not compared - each implementation maps
+its own type object to `decoded`, so the comparison does not depend on one
+language's representation.
+
+- `valid: true` - the parser succeeds and the decoded type equals `decoded`. A
+ type an implementation does not model is UNSUPPORTED, not a failure. If the
case
+ also carries `canonical`, re-serializing the parsed type must equal it byte
for
+ byte (the write direction).
+- `valid: false` - the parser must reject `input`. A rejection passes; a
+ successful parse fails.
+
+`canonical` is present only where the spec pins one spelling. Appendix C's
canonical
+for `decimal` is the spaced `decimal(<P>, <S>)`; the no-space `decimal(9,2)`
is an
+accepted read form that re-serializes to the canonical `decimal(9, 2)`.
+
+## Scope
+
+Each type in isolation, per the Primitive Types table and Appendix C. The full
+schema document (schema-id, identifier-field-ids, field ordering) is the
`schema`
+surface. Whether a type is legal at a given format version is not decided here,
+because a type in isolation carries no version.
+
+## Inputs
+
+- `primitive/` - every v1/v2 primitive (`boolean`, `int`, `long`, `float`,
+ `double`, `date`, `time`, `timestamp`, `timestamptz`, `string`, `uuid`,
+ `binary`, `decimal`, `fixed`) plus the v3 additions `timestamp_ns`,
+ `timestamptz_ns`, `unknown`.
+- `variant/` - `variant` (v3).
+- `nested/` - `struct`, `list`, `map`, including nesting (a `struct` field
whose
+ type is a `list`).
+- `geospatial/` - `geometry` and `geography` (v3), with explicit and default
CRS.
+
+## Case format
+
+One `cases.json` per directory, a JSON object with a `cases` array:
+
+| field | meaning |
+| --- | --- |
+| `id` | case id, unique within the surface |
+| `valid` | `true` if the parser must accept `input`, `false` if it must
reject it |
+| `input` | the type string (`"decimal(9,2)"`), or a JSON object for a nested
type |
+| `decoded` | the decoded shape; present only when `valid` is `true` |
+| `canonical` | the exact re-serialized string; present only where the spec
pins one spelling |
+| `clause` | the spec rule this case pins |
+| `spec_ref` | anchor into `format/spec.md` |
+| `normative_level` | optional; `must` (default) or `should` for an advisory
case a conformant reader may reject |
+
+`decoded` is language-neutral:
+
+- a simple type with no parameters: `{"type": "<name>"}`, e.g. `{"type":
"int"}`
+- `decimal`: `{"type": "decimal", "precision": P, "scale": S}`
+- `fixed`: `{"type": "fixed", "length": L}`
+- `geometry`: `{"type": "geometry", "crs": C}`; `geography` adds `"algorithm":
A`
+- `struct`: `{"type": "struct", "fields": [{"id", "name", "required", "type"},
...]}`
+- `list`: `{"type": "list", "element-id", "element-required", "element"}`
+- `map`: `{"type": "map", "key-id", "key", "value-id", "value-required",
"value"}`
+
+A nested type's child `type` values are the same shape, recursively.
+
+Case ids are unique within a surface (the top-level directory under
`table-spec/`),
+so a future surface may reuse a name like `int` or `string`.
+
+Cases are normative MUST by default. A case marked `normative_level: "should"`
is
+advisory: the spec only recommends the behavior, so a reader that diverges is
still
+conformant. Example: `decimal( 9 , 2 )`, since readers *should* accept optional
+whitespace around parameters and separators (Appendix C). A runner reports one
of
+`pass`, `fail` (a MUST violation, the only state that makes a run nonzero),
+`advisory_fail` (an unmet SHOULD, reported but never blocking), or `skip` (the
case was
+not run: the surface is not subscribed, or the type is absent in the
implementation), so
+the advisory tier stays visible rather than reading as inert.
+
+## Provenance
+
+`input` and `decoded` are derived from `format/spec.md` (the Primitive Types
+table and Appendix C), cross-checked against Apache Iceberg Java. There is no
+binary artifact; Java is a cross-check, not the source of the inputs.
+
+## Left out on purpose
+
+Inputs the spec neither permits nor forbids, so no answer can be spec-derived:
+
+- `scale > precision`, e.g. `decimal(5, 10)`.
+- lower bounds, e.g. `decimal(0, 0)` / `fixed[0]`.
+- keyword case, e.g. `DECIMAL(9,2)`.
+- geospatial CRS *quoting*, e.g. `geometry('OGC:CRS84')`. The spec's canonical
+ form is unquoted (`geometry(OGC:CRS84)`), which the shipped cases use;
whether
+ the quoted form is also accepted is unpinned.
diff --git a/table-spec/types/geospatial/cases.json
b/table-spec/types/geospatial/cases.json
new file mode 100644
index 0000000..20a0f54
--- /dev/null
+++ b/table-spec/types/geospatial/cases.json
@@ -0,0 +1,120 @@
+{
+ "cases": [
+ {
+ "id": "geometry-crs84",
+ "valid": true,
+ "input": "geometry(OGC:CRS84)",
+ "decoded": {"type": "geometry", "crs": "OGC:CRS84"},
+ "canonical": "geometry(OGC:CRS84)",
+ "clause": "geometry(C) with explicit CRS; the canonical serialized form
is the unquoted \"geometry(<C>)\"",
+ "spec_ref": "format/spec.md#appendix-c-json-serialization"
+ },
+ {
+ "id": "geometry-srid",
+ "valid": true,
+ "input": "geometry(srid:4326)",
+ "decoded": {"type": "geometry", "crs": "srid:4326"},
+ "canonical": "geometry(srid:4326)",
+ "clause": "geometry(C) example from Appendix C is the unquoted
\"geometry(srid:4326)\"",
+ "spec_ref": "format/spec.md#appendix-c-json-serialization"
+ },
+ {
+ "id": "geometry-default-crs",
+ "valid": true,
+ "input": "geometry",
+ "decoded": {"type": "geometry", "crs": "OGC:CRS84"},
+ "clause": "geometry(C): if C is not specified, C is OGC:CRS84
(spec.md:284-285); the read/parse direction is a MUST",
+ "spec_ref": "format/spec.md#primitive-types"
+ },
+ {
+ "id": "geometry-default-crs-write",
+ "valid": true,
+ "normative_level": "should",
+ "input": "geometry",
+ "decoded": {"type": "geometry", "crs": "OGC:CRS84"},
+ "canonical": "geometry(OGC:CRS84)",
+ "clause": "geometry(C) re-serialize: Appendix C's canonical is fully
parameterized, but whether a writer may elide the default is unsettled, so the
write direction is advisory",
+ "spec_ref": "format/spec.md#appendix-c-json-serialization"
+ },
+ {
+ "id": "geography-crs84-spherical",
+ "valid": true,
+ "input": "geography(OGC:CRS84, spherical)",
+ "decoded": {"type": "geography", "crs": "OGC:CRS84", "algorithm":
"spherical"},
+ "canonical": "geography(OGC:CRS84, spherical)",
+ "clause": "geography(C, A) with explicit CRS and algorithm; the
canonical serialized form is the unquoted \"geography(<C>, <A>)\"",
+ "spec_ref": "format/spec.md#appendix-c-json-serialization"
+ },
+ {
+ "id": "geography-default",
+ "valid": true,
+ "input": "geography",
+ "decoded": {"type": "geography", "crs": "OGC:CRS84", "algorithm":
"spherical"},
+ "clause": "geography(C, A): if not specified, C is OGC:CRS84 and A is
spherical (spec.md:284-285); the read/parse direction is a MUST",
+ "spec_ref": "format/spec.md#primitive-types"
+ },
+ {
+ "id": "geography-default-write",
+ "valid": true,
+ "normative_level": "should",
+ "input": "geography",
+ "decoded": {"type": "geography", "crs": "OGC:CRS84", "algorithm":
"spherical"},
+ "canonical": "geography(OGC:CRS84, spherical)",
+ "clause": "geography(C, A) re-serialize: Appendix C's canonical is fully
parameterized, but whether a writer may elide the defaults is unsettled, so the
write direction is advisory",
+ "spec_ref": "format/spec.md#appendix-c-json-serialization"
+ },
+ {
+ "id": "geography-crs-only",
+ "valid": true,
+ "normative_level": "should",
+ "input": "geography(OGC:CRS84)",
+ "decoded": {"type": "geography", "crs": "OGC:CRS84", "algorithm":
"spherical"},
+ "canonical": "geography(OGC:CRS84, spherical)",
+ "clause": "geography(C): the 1-arg form (CRS given, algorithm omitted)
is not in Appendix C grammar; cross-checked against Java reference behavior,
which defaults A to spherical and re-serializes to the Appendix C canonical
geography(<C>, <A>)",
+ "spec_ref": "format/spec.md#appendix-c-json-serialization"
+ },
+ {
+ "id": "geography-vincenty",
+ "valid": true,
+ "input": "geography(OGC:CRS84, vincenty)",
+ "decoded": {"type": "geography", "crs": "OGC:CRS84", "algorithm":
"vincenty"},
+ "canonical": "geography(OGC:CRS84, vincenty)",
+ "clause": "geography edge-interpolation algorithm vincenty (a v3 allowed
algorithm)",
+ "spec_ref": "format/spec.md#primitive-types"
+ },
+ {
+ "id": "geography-thomas",
+ "valid": true,
+ "input": "geography(OGC:CRS84, thomas)",
+ "decoded": {"type": "geography", "crs": "OGC:CRS84", "algorithm":
"thomas"},
+ "canonical": "geography(OGC:CRS84, thomas)",
+ "clause": "geography edge-interpolation algorithm thomas (a v3 allowed
algorithm)",
+ "spec_ref": "format/spec.md#primitive-types"
+ },
+ {
+ "id": "geography-andoyer",
+ "valid": true,
+ "input": "geography(OGC:CRS84, andoyer)",
+ "decoded": {"type": "geography", "crs": "OGC:CRS84", "algorithm":
"andoyer"},
+ "canonical": "geography(OGC:CRS84, andoyer)",
+ "clause": "geography edge-interpolation algorithm andoyer (a v3 allowed
algorithm)",
+ "spec_ref": "format/spec.md#primitive-types"
+ },
+ {
+ "id": "geography-karney",
+ "valid": true,
+ "input": "geography(OGC:CRS84, karney)",
+ "decoded": {"type": "geography", "crs": "OGC:CRS84", "algorithm":
"karney"},
+ "canonical": "geography(OGC:CRS84, karney)",
+ "clause": "geography edge-interpolation algorithm karney (a v3 allowed
algorithm)",
+ "spec_ref": "format/spec.md#primitive-types"
+ },
+ {
+ "id": "geography-unknown-algorithm",
+ "valid": false,
+ "input": "geography(OGC:CRS84, bogus)",
+ "clause": "geography edge-interpolation algorithm A must be one of the
closed set (spherical, vincenty, thomas, andoyer, karney); an out-of-set value
is rejected",
+ "spec_ref": "format/spec.md#primitive-types"
+ }
+ ]
+}
diff --git a/table-spec/types/nested/cases.json
b/table-spec/types/nested/cases.json
new file mode 100644
index 0000000..cb1d3b5
--- /dev/null
+++ b/table-spec/types/nested/cases.json
@@ -0,0 +1,96 @@
+{
+ "cases": [
+ {
+ "id": "struct-single-field",
+ "valid": true,
+ "input": {"type": "struct", "fields": [{"id": 1, "name": "a",
"required": true, "type": "int"}]},
+ "decoded": {"type": "struct", "fields": [{"id": 1, "name": "a",
"required": true, "type": {"type": "int"}}]},
+ "clause": "struct is a tuple of typed fields; each field has an integer
id, a name, a required flag, and a type",
+ "spec_ref": "format/spec.md#nested-types"
+ },
+ {
+ "id": "struct-empty",
+ "valid": true,
+ "input": {"type": "struct", "fields": []},
+ "decoded": {"type": "struct", "fields": []},
+ "clause": "a struct's fields array may be empty (Appendix C struct
serialization)",
+ "spec_ref": "format/spec.md#appendix-c-json-serialization"
+ },
+ {
+ "id": "struct-optional-field",
+ "valid": true,
+ "input": {"type": "struct", "fields": [{"id": 1, "name": "a",
"required": false, "type": "string"}]},
+ "decoded": {"type": "struct", "fields": [{"id": 1, "name": "a",
"required": false, "type": {"type": "string"}}]},
+ "clause": "each struct field can be optional or required (required=false
permits null values)",
+ "spec_ref": "format/spec.md#nested-types"
+ },
+ {
+ "id": "list-required-element",
+ "valid": true,
+ "input": {"type": "list", "element-id": 2, "element-required": true,
"element": "string"},
+ "decoded": {"type": "list", "element-id": 2, "element-required": true,
"element": {"type": "string"}},
+ "clause": "a list has an element type; the element field has an integer
id and a required flag",
+ "spec_ref": "format/spec.md#appendix-c-json-serialization"
+ },
+ {
+ "id": "list-optional-element",
+ "valid": true,
+ "input": {"type": "list", "element-id": 2, "element-required": false,
"element": "long"},
+ "decoded": {"type": "list", "element-id": 2, "element-required": false,
"element": {"type": "long"}},
+ "clause": "list elements can be optional or required",
+ "spec_ref": "format/spec.md#nested-types"
+ },
+ {
+ "id": "map-string-double",
+ "valid": true,
+ "input": {"type": "map", "key-id": 3, "key": "string", "value-id": 4,
"value-required": false, "value": "double"},
+ "decoded": {"type": "map", "key-id": 3, "key": {"type": "string"},
"value-id": 4, "value-required": false, "value": {"type": "double"}},
+ "clause": "a map has a key type and a value type; keys are required,
values may be optional or required",
+ "spec_ref": "format/spec.md#appendix-c-json-serialization"
+ },
+ {
+ "id": "struct-nested-list",
+ "valid": true,
+ "input": {"type": "struct", "fields": [{"id": 1, "name": "tags",
"required": true, "type": {"type": "list", "element-id": 2, "element-required":
true, "element": "string"}}]},
+ "decoded": {"type": "struct", "fields": [{"id": 1, "name": "tags",
"required": true, "type": {"type": "list", "element-id": 2, "element-required":
true, "element": {"type": "string"}}}]},
+ "clause": "fields may be any type, including nested types (a struct
field whose type is a list)",
+ "spec_ref": "format/spec.md#nested-types"
+ },
+ {
+ "id": "struct-field-missing-id",
+ "valid": false,
+ "input": {"type": "struct", "fields": [{"name": "a", "required": true,
"type": "int"}]},
+ "clause": "each field in a struct has an integer id; a field without an
id is invalid",
+ "spec_ref": "format/spec.md#nested-types"
+ },
+ {
+ "id": "map-string-double-required",
+ "valid": true,
+ "input": {"type": "map", "key-id": 3, "key": "string", "value-id": 4,
"value-required": true, "value": "double"},
+ "decoded": {"type": "map", "key-id": 3, "key": {"type": "string"},
"value-id": 4, "value-required": true, "value": {"type": "double"}},
+ "clause": "map values may be optional or required (value-required=true)",
+ "spec_ref": "format/spec.md#nested-types"
+ },
+ {
+ "id": "list-missing-element-id",
+ "valid": false,
+ "input": {"type": "list", "element-required": true, "element": "string"},
+ "clause": "a list's element field has an integer element-id; a list
without element-id is invalid",
+ "spec_ref": "format/spec.md#appendix-c-json-serialization"
+ },
+ {
+ "id": "map-missing-key-id",
+ "valid": false,
+ "input": {"type": "map", "key": "string", "value-id": 4,
"value-required": false, "value": "double"},
+ "clause": "a map's key field has an integer key-id; a map without key-id
is invalid",
+ "spec_ref": "format/spec.md#appendix-c-json-serialization"
+ },
+ {
+ "id": "map-missing-value-id",
+ "valid": false,
+ "input": {"type": "map", "key-id": 3, "key": "string", "value-required":
false, "value": "double"},
+ "clause": "a map's value field has an integer value-id; a map without
value-id is invalid",
+ "spec_ref": "format/spec.md#appendix-c-json-serialization"
+ }
+ ]
+}
diff --git a/table-spec/types/primitive/cases.json
b/table-spec/types/primitive/cases.json
new file mode 100644
index 0000000..0b80383
--- /dev/null
+++ b/table-spec/types/primitive/cases.json
@@ -0,0 +1,34 @@
+{
+ "cases": [
+ { "id": "boolean", "valid": true, "input": "boolean", "decoded": { "type":
"boolean" }, "canonical": "boolean", "clause": "Primitive Types: boolean;
Appendix C canonical string", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "int", "valid": true, "input": "int", "decoded": { "type": "int"
}, "canonical": "int", "clause": "Primitive Types: int; Appendix C canonical
string", "spec_ref": "format/spec.md#appendix-c-json-serialization" },
+ { "id": "long", "valid": true, "input": "long", "decoded": { "type":
"long" }, "canonical": "long", "clause": "Primitive Types: long; Appendix C
canonical string", "spec_ref": "format/spec.md#appendix-c-json-serialization" },
+ { "id": "float", "valid": true, "input": "float", "decoded": { "type":
"float" }, "canonical": "float", "clause": "Primitive Types: float; Appendix C
canonical string", "spec_ref": "format/spec.md#appendix-c-json-serialization" },
+ { "id": "double", "valid": true, "input": "double", "decoded": { "type":
"double" }, "canonical": "double", "clause": "Primitive Types: double; Appendix
C canonical string", "spec_ref": "format/spec.md#appendix-c-json-serialization"
},
+ { "id": "date", "valid": true, "input": "date", "decoded": { "type":
"date" }, "canonical": "date", "clause": "Primitive Types: date; Appendix C
canonical string", "spec_ref": "format/spec.md#appendix-c-json-serialization" },
+ { "id": "time", "valid": true, "input": "time", "decoded": { "type":
"time" }, "canonical": "time", "clause": "Primitive Types: time; Appendix C
canonical string", "spec_ref": "format/spec.md#appendix-c-json-serialization" },
+ { "id": "timestamp", "valid": true, "input": "timestamp", "decoded": {
"type": "timestamp" }, "canonical": "timestamp", "clause": "Primitive Types:
timestamp; Appendix C canonical string", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "timestamptz", "valid": true, "input": "timestamptz", "decoded": {
"type": "timestamptz" }, "canonical": "timestamptz", "clause": "Primitive
Types: timestamptz; Appendix C canonical string", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "timestamp_ns", "valid": true, "input": "timestamp_ns", "decoded":
{ "type": "timestamp_ns" }, "canonical": "timestamp_ns", "clause": "Primitive
Types: timestamp_ns added in v3; Appendix C canonical string", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "timestamptz_ns", "valid": true, "input": "timestamptz_ns",
"decoded": { "type": "timestamptz_ns" }, "canonical": "timestamptz_ns",
"clause": "Primitive Types: timestamptz_ns added in v3; Appendix C canonical
string", "spec_ref": "format/spec.md#appendix-c-json-serialization" },
+ { "id": "string", "valid": true, "input": "string", "decoded": { "type":
"string" }, "canonical": "string", "clause": "Primitive Types: string; Appendix
C canonical string", "spec_ref": "format/spec.md#appendix-c-json-serialization"
},
+ { "id": "uuid", "valid": true, "input": "uuid", "decoded": { "type":
"uuid" }, "canonical": "uuid", "clause": "Primitive Types: uuid; Appendix C
canonical string", "spec_ref": "format/spec.md#appendix-c-json-serialization" },
+ { "id": "binary", "valid": true, "input": "binary", "decoded": { "type":
"binary" }, "canonical": "binary", "clause": "Primitive Types: binary; Appendix
C canonical string", "spec_ref": "format/spec.md#appendix-c-json-serialization"
},
+ { "id": "unknown", "valid": true, "input": "unknown", "decoded": { "type":
"unknown" }, "canonical": "unknown", "clause": "Primitive Types: unknown added
in v3; Appendix C canonical string", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "fixed-1", "valid": true, "input": "fixed[1]", "decoded": {
"type": "fixed", "length": 1 }, "canonical": "fixed[1]", "clause": "Appendix C:
fixed canonical string is fixed[<L>]", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "fixed-16", "valid": true, "input": "fixed[16]", "decoded": {
"type": "fixed", "length": 16 }, "canonical": "fixed[16]", "clause": "Appendix
C: fixed canonical string is fixed[<L>]", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "decimal-9-2", "valid": true, "input": "decimal(9,2)", "decoded":
{ "type": "decimal", "precision": 9, "scale": 2 }, "canonical": "decimal(9,
2)", "clause": "Appendix C: the canonical serialized form is the spaced
decimal(<P>, <S>); the no-space input re-serializes to it", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "decimal-9-2-spaced", "valid": true, "input": "decimal(9, 2)",
"decoded": { "type": "decimal", "precision": 9, "scale": 2 }, "canonical":
"decimal(9, 2)", "clause": "Appendix C canonical form is the spaced decimal(9,
2); input is already canonical and re-serializes unchanged", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "decimal-9-2-spaced-params", "valid": true, "normative_level":
"should", "input": "decimal( 9 , 2 )", "decoded": { "type": "decimal",
"precision": 9, "scale": 2 }, "clause": "Appendix C: readers should accept
optional whitespace around parameters and separators; a reader that rejects
decimal( 9 , 2 ) is still conformant", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "decimal-38-0", "valid": true, "input": "decimal(38,0)",
"decoded": { "type": "decimal", "precision": 38, "scale": 0 }, "clause":
"Primitive Types: decimal precision must be 38 or less (38 is the maximum)",
"spec_ref": "format/spec.md#primitive-types" },
+ { "id": "decimal-precision-over-38", "valid": false, "input":
"decimal(39,0)", "clause": "Primitive Types: decimal precision must be 38 or
less", "spec_ref": "format/spec.md#primitive-types" },
+ { "id": "decimal-missing-scale", "valid": false, "input": "decimal(9)",
"clause": "Appendix C: decimal is written decimal(P,S); scale is required",
"spec_ref": "format/spec.md#appendix-c-json-serialization" },
+ { "id": "decimal-empty-params", "valid": false, "input": "decimal()",
"clause": "Appendix C: decimal requires precision and scale", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "decimal-non-numeric", "valid": false, "input": "decimal(a,b)",
"clause": "Appendix C: decimal precision and scale are integers", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "fixed-empty-length", "valid": false, "input": "fixed[]",
"clause": "Appendix C: fixed is written fixed[<L>]; length is required",
"spec_ref": "format/spec.md#appendix-c-json-serialization" },
+ { "id": "fixed-unterminated", "valid": false, "input": "fixed[16",
"clause": "Appendix C: fixed[<L>] must be closed with a bracket", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "fixed-non-numeric", "valid": false, "input": "fixed[abc]",
"clause": "Appendix C: fixed length is an integer", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "empty-type", "valid": false, "input": "", "clause": "Primitive
Types: the empty string is not a type name", "spec_ref":
"format/spec.md#primitive-types" },
+ { "id": "unknown-type-name", "valid": false, "input": "notatype",
"clause": "Primitive Types: only the listed type names are valid", "spec_ref":
"format/spec.md#primitive-types" }
+ ]
+}
diff --git a/table-spec/types/variant/cases.json
b/table-spec/types/variant/cases.json
new file mode 100644
index 0000000..4d92865
--- /dev/null
+++ b/table-spec/types/variant/cases.json
@@ -0,0 +1,6 @@
+{
+ "cases": [
+ { "id": "variant", "valid": true, "input": "variant", "decoded": { "type":
"variant" }, "canonical": "variant", "clause": "Semi-structured Types: variant
added in v3; Appendix C canonical string", "spec_ref":
"format/spec.md#appendix-c-json-serialization" },
+ { "id": "variant-with-params", "valid": false, "input": "variant(x)",
"clause": "Appendix C: variant is a bare type name and takes no parameters",
"spec_ref": "format/spec.md#appendix-c-json-serialization" }
+ ]
+}