# Schema Evolution and Management

**Last updated:** 2026-02
**Status:** GA
**Category:** Data Engineering for AI

---

## Introduksjon

Skjemaendringer er uunngaaelige i moderne dataarkitekturer: nye kolonner legges til, datatyper endres, kolonner gis nye navn, og foreldede felt fjernes. For AI-pipelines er dette spesielt utfordrende fordi ML-modeller er trent pa spesifikke feature-skjemaer, og enhver skjemaendring kan bryte trenings- og inferens-pipelines. Delta Lake i Microsoft Fabric og Azure Databricks tilbyr robust stotte for skjemaevolusjon som gjor det mulig a haandtere disse endringene uten nedetid.

Schema enforcement (skjemahindring) sikrer at data som skrives til en tabell matcher forventet skjema, mens schema evolution (skjemaevolusjon) lar tabellskjemaet tilpasse seg nye datastrukturer automatisk. Kombinasjonen av disse to mekanismene gir en kontrollert tilnaerming der daarlig data avvises mens legitime strukturendringer aksepteres.

For norsk offentlig sektor, der datakvalitet og sporbarhet er lovpalagt, er det kritisk a ha en systematisk tilnaerming til skjemahondtering. Delta Lake sin transaksjonslogg gir full audit trail over alle skjemaendringer, noe som stotter krav i Forvaltningsloven og Arkivlova.

---

## Schema Versioning and Compatibility Levels

### Skjemaevolusjon i Delta Lake

Delta Lake stotter folgende typer skjemaendringer:

| Endringstype | Schema Enforcement | Schema Evolution | Kommentar |
|-------------|-------------------|-----------------|-----------|
| **Ny kolonne** | Blokkerer skriving | Legger til automatisk | Vanligste endring |
| **Kolonnenavn-endring** | N/A | Via column mapping | Krever DDL |
| **Slettet kolonne** | N/A | Via column mapping | Krever DDL |
| **Type-utvidelse** | Blokkerer skriving | Type widening | INT -> BIGINT |
| **Type-endring** | Blokkerer skriving | overwriteSchema | Destruktiv |

### Kompatibilitetsnivaaer

```
+---------------------------------------------------+
| BACKWARD COMPATIBLE (trygt)                       |
|   - Legge til nye nullable-kolonner               |
|   - Utvide datatyper (INT -> BIGINT -> DOUBLE)   |
|                                                   |
| FORWARD COMPATIBLE (krever koordinering)          |
|   - Gi nytt navn til kolonner                     |
|   - Fjerne kolonner                               |
|                                                   |
| BREAKING CHANGES (krever migrasjon)               |
|   - Endre datatype (STRING -> INT)               |
|   - Endre nullability (nullable -> not null)      |
|   - Omstrukturere nesting                         |
+---------------------------------------------------+
```

### Delta Lake Protocol Versions

Delta Lake bruker protokollversjoner for a kontrollere funksjonskompatibilitet:

| Feature | minReaderVersion | minWriterVersion | Beskrivelse |
|---------|-----------------|-----------------|-------------|
| Column Mapping | 2 | 5 | Kolonnenavn-endring og sletting |
| Type Widening | 3 | 7 | Automatisk type-utvidelse |
| Table Features | 3 | 7 | Granular feature-kontroll |
| Liquid Clustering | 2 | 7 | Dynamisk clustering |

```sql
-- Sjekk gjeldende protokollversjoner
DESCRIBE DETAIL lakehouse.default.ml_features;

-- Oppgrader protokoll for a stotte column mapping
ALTER TABLE lakehouse.default.ml_features
SET TBLPROPERTIES (
    'delta.minReaderVersion' = '2',
    'delta.minWriterVersion' = '5',
    'delta.columnMapping.mode' = 'name'
);
```

---

## Adding Columns with Default Values

### Automatisk skjemaevolusjon ved skriving

```python
# Aktiver schema evolution for en skriveoperasjon
df_with_new_column = df.withColumn("weather_score", F.lit(0.0))

# Med mergeSchema: Legger til ny kolonne automatisk
df_with_new_column.write \
    .format("delta") \
    .option("mergeSchema", "true") \
    .mode("append") \
    .saveAsTable("lakehouse.default.ml_features")
```

### Legge til kolonner via DDL

```sql
-- Legg til ny kolonne med kommentar
ALTER TABLE lakehouse.default.ml_features
ADD COLUMN weather_score DOUBLE
COMMENT 'Vaerscore 0-1 for prediksjonskvalitet';

-- Legg til flere kolonner samtidig
ALTER TABLE lakehouse.default.ml_features
ADD COLUMNS (
    model_version STRING COMMENT 'Versjon av ML-modellen',
    confidence_score DOUBLE COMMENT 'Konfidensintervall 0-1',
    processing_timestamp TIMESTAMP COMMENT 'Tidspunkt for prosessering'
);

-- Legg til kolonne med generert verdi
ALTER TABLE lakehouse.default.ml_features
ADD COLUMN year_month STRING
GENERATED ALWAYS AS (DATE_FORMAT(created_date, 'yyyy-MM'));
```

### Backfill av nye kolonner

```python
from delta.tables import DeltaTable

def backfill_column(table_name, column_name, default_value=None, compute_func=None):
    """
    Fyll ny kolonne med verdier for eksisterende rader.

    Args:
        table_name: Tabellnavn
        column_name: Kolonnenavn
        default_value: Statisk standardverdi
        compute_func: Funksjon for a beregne verdi basert pa andre kolonner
    """
    delta_table = DeltaTable.forName(spark, table_name)

    if default_value is not None:
        delta_table.update(
            condition=F.col(column_name).isNull(),
            set={column_name: F.lit(default_value)}
        )
    elif compute_func is not None:
        delta_table.update(
            condition=F.col(column_name).isNull(),
            set={column_name: compute_func}
        )

# Eksempler
# Statisk standardverdi
backfill_column("lakehouse.default.ml_features", "weather_score", default_value=0.5)

# Beregnet verdi basert pa andre kolonner
backfill_column(
    "lakehouse.default.ml_features",
    "confidence_score",
    compute_func=F.when(F.col("prediction_count") > 100, 0.9).otherwise(0.5)
)
```

---

## Type Promotions and Narrowing

### Stottede type-utvidelser (Type Widening)

Delta Lake stotter folgende trygge type-utvidelser:

| Fra | Til | Automatisk | Kommentar |
|-----|-----|-----------|-----------|
| BYTE | SHORT | Ja | Uten datatap |
| SHORT | INT | Ja | Uten datatap |
| INT | LONG | Ja | Uten datatap |
| LONG | DECIMAL | Betinget | Desimalbredde ma vaere tilstrekkelig |
| FLOAT | DOUBLE | Ja | Uten datatap |
| DATE | TIMESTAMP | Ja | Legger til tid 00:00:00 |
| DECIMAL(p,s) | DECIMAL(p',s') | Ja | Hvis p'>=p og s'>=s |

```sql
-- Aktiver type widening pa tabellen
ALTER TABLE lakehouse.default.ml_features
SET TBLPROPERTIES ('delta.enableTypeWidening' = 'true');

-- Na kan du skrive data med bredere typer
-- F.eks. INT-kolonner aksepterer LONG-verdier automatisk
```

### Type-utvidelse med Schema Evolution

```python
# Automatisk type-utvidelse under merge
spark.conf.set("spark.databricks.delta.schema.autoMerge.enabled", "true")

# Na vil en DataFrame med LONG-verdi for en INT-kolonne
# automatisk utvide kolonnetypen
df_new = spark.createDataFrame([
    (1, 3000000000, "test")  # 3 milliarder overskrider INT
], ["id", "large_count", "name"])

# Merge med schema evolution og type widening
delta_table = DeltaTable.forName(spark, "lakehouse.default.counts")
delta_table.alias("target").merge(
    df_new.alias("source"),
    "target.id = source.id"
).whenMatchedUpdateAll() \
 .whenNotMatchedInsertAll() \
 .execute()
```

### Type-innsnevring (farlig)

Type-innsnevring (f.eks. LONG -> INT) kan fore til datatap og krever full overskriving:

```python
# ADVARSEL: Dette overskriver hele tabellskjemaet
df_narrowed = spark.table("lakehouse.default.legacy_table") \
    .withColumn("count_col", F.col("count_col").cast("int"))

df_narrowed.write \
    .format("delta") \
    .option("overwriteSchema", "true") \
    .mode("overwrite") \
    .saveAsTable("lakehouse.default.legacy_table")
```

---

## Deprecated Column Handling

### Column Mapping for sikker kolonnefjerning

```sql
-- Aktiver column mapping (kreves for rename/drop)
ALTER TABLE lakehouse.default.ml_features
SET TBLPROPERTIES (
    'delta.columnMapping.mode' = 'name'
);

-- Gi nytt navn til en kolonne
ALTER TABLE lakehouse.default.ml_features
RENAME COLUMN old_feature_name TO new_feature_name;

-- Slett en kolonne (logisk, ingen data-omskriving)
ALTER TABLE lakehouse.default.ml_features
DROP COLUMN deprecated_feature;

-- Slett flere kolonner
ALTER TABLE lakehouse.default.ml_features
DROP COLUMNS (temp_col1, temp_col2, debug_flag);
```

### Soft Deprecation-moenster

For gradvis utfasing av kolonner i AI-pipelines:

```python
# Trinn 1: Merk kolonne som deprecated via kommentar
spark.sql("""
    ALTER TABLE lakehouse.default.ml_features
    ALTER COLUMN old_score COMMENT 'DEPRECATED: Bruk new_score i stedet. Fjernes 2026-06-01.'
""")

# Trinn 2: Legg til ny kolonne med forbedret logikk
spark.sql("""
    ALTER TABLE lakehouse.default.ml_features
    ADD COLUMN new_score DOUBLE COMMENT 'Erstatter old_score med forbedret beregning'
""")

# Trinn 3: Backfill ny kolonne
delta_table = DeltaTable.forName(spark, "lakehouse.default.ml_features")
delta_table.update(
    set={"new_score": F.col("old_score") * 1.1}  # Eksempel: justert beregning
)

# Trinn 4: Oppdater downstream-pipelines til a bruke new_score
# (Gjoeres over tid, ikke alt pa en gang)

# Trinn 5: Etter overgangsperiode - fjern gammel kolonne
# spark.sql("ALTER TABLE lakehouse.default.ml_features DROP COLUMN old_score")
```

### Kolonneregistrering for ML Feature Store

```python
# Hold styr pa hvilke kolonner som er aktive, deprecated, eller fjernet
feature_registry = {
    "ml_features": {
        "active": [
            {"name": "traffic_volume", "type": "DOUBLE", "since": "2025-01"},
            {"name": "weather_score", "type": "DOUBLE", "since": "2025-06"},
            {"name": "road_condition_index", "type": "DOUBLE", "since": "2025-03"},
            {"name": "new_score", "type": "DOUBLE", "since": "2026-01"}
        ],
        "deprecated": [
            {"name": "old_score", "type": "DOUBLE", "since": "2025-01",
             "deprecated_date": "2026-01", "removal_date": "2026-06",
             "replacement": "new_score"}
        ],
        "removed": [
            {"name": "temp_debug_col", "type": "STRING",
             "removed_date": "2025-12", "reason": "Debug-kolonne, ikke lenger noedvendig"}
        ]
    }
}
```

---

## Schema Registration and Validation

### Skjemavalidering i pipelines

```python
from pyspark.sql.types import StructType, StructField, StringType, DoubleType, TimestampType, LongType

def validate_schema(df, expected_schema: StructType, strict: bool = False):
    """
    Valider at en DataFrame matcher forventet skjema.

    Args:
        df: DataFrame a validere
        expected_schema: Forventet StructType
        strict: Hvis True, avvis ekstra kolonner. Hvis False, tillat ekstra.
    """
    actual_fields = {f.name: f for f in df.schema.fields}
    expected_fields = {f.name: f for f in expected_schema.fields}

    errors = []

    # Sjekk at alle forventede kolonner finnes
    for name, expected_field in expected_fields.items():
        if name not in actual_fields:
            errors.append(f"Mangler kolonne: {name} ({expected_field.dataType})")
        else:
            actual_field = actual_fields[name]
            # Sjekk datatype
            if actual_field.dataType != expected_field.dataType:
                errors.append(
                    f"Type-mismatch for '{name}': "
                    f"forventet {expected_field.dataType}, fikk {actual_field.dataType}"
                )
            # Sjekk nullability
            if not expected_field.nullable and actual_field.nullable:
                errors.append(
                    f"Nullability-mismatch for '{name}': "
                    f"forventet NOT NULL, fikk NULLABLE"
                )

    # Sjekk for uventede kolonner
    if strict:
        extra_cols = set(actual_fields.keys()) - set(expected_fields.keys())
        if extra_cols:
            errors.append(f"Uventede kolonner: {extra_cols}")

    return {
        "valid": len(errors) == 0,
        "errors": errors,
        "actual_columns": len(actual_fields),
        "expected_columns": len(expected_fields)
    }

# Definer forventet skjema for ML features
expected_feature_schema = StructType([
    StructField("entity_id", StringType(), nullable=False),
    StructField("feature_timestamp", TimestampType(), nullable=False),
    StructField("traffic_volume", DoubleType(), nullable=True),
    StructField("weather_score", DoubleType(), nullable=True),
    StructField("road_condition_index", DoubleType(), nullable=True),
    StructField("prediction_target", DoubleType(), nullable=False)
])

# Valider incoming data
result = validate_schema(incoming_df, expected_feature_schema, strict=False)
if not result["valid"]:
    raise ValueError(f"Skjemavalidering feilet: {result['errors']}")
```

### Schema evolution i Structured Streaming

```python
# Auto Loader med skjemaevolusjon
df_stream = spark.readStream \
    .format("cloudFiles") \
    .option("cloudFiles.format", "json") \
    .option("cloudFiles.schemaLocation", "/checkpoints/schema/") \
    .option("cloudFiles.schemaEvolutionMode", "addNewColumns") \
    .option("cloudFiles.schemaHints", "event_id STRING, timestamp TIMESTAMP") \
    .load("/landing/events/")

# Skriv med schema evolution aktivert
df_stream.writeStream \
    .format("delta") \
    .option("checkpointLocation", "/checkpoints/events/") \
    .option("mergeSchema", "true") \
    .outputMode("append") \
    .toTable("lakehouse.default.events")
```

### Schema evolution per komponent-oversikt

| Komponent | Nye kolonner | Rename | Drop | Type-utvidelse |
|-----------|-------------|--------|------|---------------|
| **Auto Loader** | Ja (restart) | Ja (restart) | Ja (soft delete) | Nei |
| **Delta Connector** | Ja (mergeSchema) | Ja (column mapping) | Ja (column mapping) | Ja (type widening) |
| **Streaming Tables** | Ja (auto) | Ja (auto) | Ja (soft delete) | Ja (type widening) |
| **Materialized Views** | Full recompute | Full recompute | Full recompute | Full recompute |
| **Delta Tables** | Ja (auto/DDL) | Ja (DDL) | Ja (DDL) | Ja (auto/DDL) |

### Skjemamigrasjon for ML-modeller

```python
class SchemaVersionManager:
    """
    Holder styr pa skjemaversjoner og sikrer at ML-modeller
    bruker kompatible skjemaer.
    """

    def __init__(self, registry_table="lakehouse.default.schema_registry"):
        self.registry_table = registry_table

    def register_schema(self, table_name: str, version: str, schema: StructType):
        """Registrer en ny skjemaversjon."""
        schema_json = schema.json()
        spark.sql(f"""
            INSERT INTO {self.registry_table}
            VALUES ('{table_name}', '{version}', '{schema_json}',
                    current_timestamp(), true)
        """)

    def get_schema(self, table_name: str, version: str = None) -> StructType:
        """Hent skjema for en spesifikk versjon (eller siste)."""
        if version:
            row = spark.sql(f"""
                SELECT schema_json FROM {self.registry_table}
                WHERE table_name = '{table_name}' AND version = '{version}'
            """).first()
        else:
            row = spark.sql(f"""
                SELECT schema_json FROM {self.registry_table}
                WHERE table_name = '{table_name}' AND is_current = true
            """).first()

        return StructType.fromJson(json.loads(row.schema_json))

    def check_compatibility(self, table_name: str, new_schema: StructType) -> dict:
        """Sjekk om nytt skjema er bakoverkompatibelt."""
        current = self.get_schema(table_name)
        current_fields = {f.name: f for f in current.fields}
        new_fields = {f.name: f for f in new_schema.fields}

        added = set(new_fields.keys()) - set(current_fields.keys())
        removed = set(current_fields.keys()) - set(new_fields.keys())

        is_backward_compatible = len(removed) == 0

        return {
            "backward_compatible": is_backward_compatible,
            "added_columns": list(added),
            "removed_columns": list(removed),
            "recommendation": "SAFE" if is_backward_compatible else "BREAKING - koordiner med downstream"
        }
```

---

## Referanser

- [Schema evolution in Azure Databricks](https://learn.microsoft.com/en-us/azure/databricks/data-engineering/schema-evolution) -- Komplett guide til skjemaevolusjon
- [What is Delta Lake?](https://learn.microsoft.com/en-us/azure/synapse-analytics/spark/apache-spark-what-is-delta-lake) -- Delta Lake features inkludert schema enforcement og evolution
- [Delta Lake feature compatibility](https://learn.microsoft.com/en-us/azure/databricks/delta/feature-compatibility) -- Protokollversjoner og table features
- [Schema enforcement](https://learn.microsoft.com/en-us/azure/databricks/tables/schema-enforcement) -- Skjemahondtering pa skrivetidspunktet
- [Column mapping](https://learn.microsoft.com/en-us/azure/databricks/delta/column-mapping) -- Rename og drop av kolonner
- [Type widening](https://learn.microsoft.com/en-us/azure/databricks/delta/type-widening) -- Automatisk type-utvidelse
- [Update Delta Lake table schema](https://learn.microsoft.com/en-us/azure/databricks/delta/update-schema) -- DDL og mergeSchema

---

## For Cosmo

- **Bruk denne referansen** naar kunder haandterer skjemaendringer i Delta Lake-tabeller, eller naar de trenger strategier for skjemaversjonering i ML-pipelines.
- **Schema enforcement + evolution er komplementaere**: Enforcement hindrer daarlig data, evolution lar skjemaet vokse. Aktiver begge for AI-datatabeller.
- **Column mapping er pabudt** for rename/drop-operasjoner. Aktiver det tidlig pa tabeller som vil utvikle seg over tid.
- **Type widening er trygt for analytics**: INT -> BIGINT og FLOAT -> DOUBLE er trygge operasjoner. Type-innsnevring bor aldri gjores automatisk.
- **For norsk offentlig sektor**: Fremhev at Delta Lake sin transaksjonslogg gir full sporbarhet over alle skjemaendringer, noe som stotter Arkivlovas krav til dokumentasjon av dataendringer.
