{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://runninglake.com/schema/v1alpha1/table.json",
  "title": "Table",
  "description": "A Table as RunningLake's configuration repository declares it, at tables/<name>.yaml. Generated from the Go types the control plane decodes this file into, so it checks the shape the gate reads — field names and types, and which fields may exist. The gate also applies rules no schema can express — required fields inside spec among them — so a file this schema accepts can still be refused by `rl diff`. Take #/properties/spec for the spec alone.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "apiVersion",
    "kind",
    "metadata"
  ],
  "properties": {
    "apiVersion": {
      "type": "string",
      "const": "runninglake.io/v1alpha1",
      "description": "The config schema's version, which is independent of the HTTP API's (rule unsupported-api-version)."
    },
    "kind": {
      "type": "string",
      "const": "Table",
      "description": "The kind this file declares. A kind that disagrees with the file's path is refused (rule kind-mismatch)."
    },
    "metadata": {
      "required": [
        "name"
      ],
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "name": {
          "type": "string",
          "pattern": "^[a-z0-9_-]{1,63}$",
          "description": "The resource's name, which is also its path segment. It must be the name this file's path gives it (rule name-mismatch)."
        },
        "labels": {
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        },
        "annotations": {
          "type": "object",
          "additionalProperties": {
            "type": "string"
          }
        }
      }
    },
    "spec": {
      "description": "Which table this is — its catalog identity — and what the catalog is told when the table is first made.",
      "type": [
        "object",
        "null"
      ],
      "additionalProperties": false,
      "properties": {
        "namespace": {
          "description": "Namespace is the multi-level Iceberg namespace: [bronze, stripe]. Its first level is the table's layer, and a table directly in a layer — [bronze] — is legal. Each level matches [a-z][a-z0-9_-]{0,62}, and a declared table's namespace has at most two levels, a layer and a schema: a policy or a Grant names a table as layer.schema.table and no deeper, so a table nested below a schema could be neither masked, filtered nor granted on.",
          "type": [
            "array",
            "null"
          ],
          "items": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "table": {
          "description": "Table names the table. Empty means metadata.name, the common case: a resource called charges in [bronze, stripe] is bronze.stripe.charges. Either way the name matches [a-z][a-z0-9_-]{0,62}.",
          "type": [
            "string",
            "null"
          ]
        },
        "routing": {
          "description": "The table's format, which is required; see format.",
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": false,
          "properties": {
            "format": {
              "description": "The table format: iceberg, hudi or ducklake, in any case. Required (rules format-required and format-unknown). Choose it once: a plan that changes the format of a table the platform has already reported is refused (rule format-is-immutable), and a Pipeline's is held to its tables' once the tree declares a Layer (rule asset-format). A change pushed straight to the repository's branch does not go through a plan, so it is not refused, and does not change a table that already exists.",
              "type": [
                "string",
                "null"
              ]
            },
            "engine": {
              "description": "An engine to pin, or empty to leave the choice to the router. On a pipeline, the authoring gate refuses a pin no rule permits for its format and operations, and names the rule that fired. Nothing runs a pipeline yet, so nothing is routed today.",
              "type": [
                "string",
                "null"
              ]
            },
            "train": {
              "description": "A release-train FAMILY to pin, written YYYY.MM (for example 2026.11), or empty to follow the train of the plane the environment runs on. A Pipeline's setting; on a Table it is read by nothing. A pin names a family and not a serial: the serials inside a family are the same engines and move with the plane, and a family does not move while a pipeline pins it. A serial (2026.11.29) is refused (rule train-pin-is-a-family), and so is a family this control plane's runtime index does not hold (train-unknown) or one outside its support window, the current family plus two (train-out-of-support). When a family is pinned, spec.routing.engine is judged against the engines that family runs, and not against the plane's.",
              "type": [
                "string",
                "null"
              ]
            },
            "consumers": {
              "description": "Who reads or writes the table, and how fresh they need it. Recorded; nothing reads it yet, so a consumer constrains no engine and no storage mode today.",
              "type": [
                "array",
                "null"
              ],
              "items": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": false,
                "properties": {
                  "engine": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "access": {
                    "description": "Either read or write. Recorded; nothing reads it yet.",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "freshnessSla": {
                    "description": "How fresh this consumer needs the table, as a Go duration string, e.g. \"5m\". Recorded; nothing reads it yet.",
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "queryShape": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "schema": {
          "description": "Schema, PartitionSpec and WriteOrder are Iceberg's own objects, passed through.",
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "anyOf": [
              {
                "type": "null"
              },
              {
                "type": "boolean"
              },
              {
                "type": "object"
              },
              {
                "type": "array"
              },
              {
                "type": "string"
              },
              {
                "type": "number"
              }
            ]
          }
        },
        "partitionSpec": {
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "anyOf": [
              {
                "type": "null"
              },
              {
                "type": "boolean"
              },
              {
                "type": "object"
              },
              {
                "type": "array"
              },
              {
                "type": "string"
              },
              {
                "type": "number"
              }
            ]
          }
        },
        "writeOrder": {
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "anyOf": [
              {
                "type": "null"
              },
              {
                "type": "boolean"
              },
              {
                "type": "object"
              },
              {
                "type": "array"
              },
              {
                "type": "string"
              },
              {
                "type": "number"
              }
            ]
          }
        },
        "location": {
          "description": "A table does not choose where it is written: the authoring gate refuses this field (rule location-asserted), and the table's folder is rendered from its layer, schema and name.",
          "type": [
            "string",
            "null"
          ]
        },
        "properties": {
          "description": "Iceberg table properties. Keys beginning rl. are the platform's: `rl diff` and the plan refuse them (rule reserved-property), as the catalog does when the table is made.",
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      }
    }
  }
}
