Steg 9 (R4): unified migrate-corpus.mjs --write over engineering/governance/ infrastructure/security. 327 filer mutert, verified=null, prosa byte-identisk (fra første ## seksjon), advisor urørt (0 endringer). To applier-fixes oppdaget under kjøring (TDD, RED→GREEN): - insertHeaderFields: anker faller nå tilbake når en meta-linje selv passerer 500B (2 filer pakket et avsnitt i **Status:** → Type/Source landet utenfor scan-vinduet, applierens post-write-assertion fanget + restaurerte). - normalizeStaleVerified: fjerner nå ALLE stale non-date **Verified:** i 500B-vinduet, inkl. stray body-dup rett under --- (9 mlops-genaiops-filer var ellers falskt "verified"/fresh, droppet fra worklist). Operatør-godkjent utvidelse av carve-out; kun stray metadata-linjer, aldri prosa. test-transform-criterion: precondition oppdatert til post-migrasjons-sannhet (fila bærer nå Source). Suite 728/728 grønn.
492 lines
18 KiB
Markdown
492 lines
18 KiB
Markdown
# Schema Evolution and Management
|
|
|
|
**Last updated:** 2026-06-24
|
|
**Status:** GA
|
|
**Category:** Data Engineering for AI
|
|
**Type:** reference
|
|
**Source:** https://learn.microsoft.com/azure/synapse-analytics/spark/apache-spark-what-is-delta-lake
|
|
|
|
---
|
|
|
|
## Innhold
|
|
|
|
- [Introduksjon](#introduksjon)
|
|
- [Schema Versioning and Compatibility Levels](#schema-versioning-and-compatibility-levels)
|
|
- [Adding Columns with Default Values](#adding-columns-with-default-values)
|
|
- [Type Promotions and Narrowing](#type-promotions-and-narrowing)
|
|
- [Deprecated Column Handling](#deprecated-column-handling)
|
|
- [Schema Registration and Validation](#schema-registration-and-validation)
|
|
- [Referanser](#referanser)
|
|
- [For Cosmo](#for-cosmo)
|
|
|
|
## 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.
|