Spark schemas¶
The Spark adaptor reads a pyspark.sql.types.StructType, or the JSON
Spark writes for one, and its members are dicts shaped like
Row.asDict(recursive=True). It never imports pyspark: a StructType
is read through its own jsonValue(), and the JSON form is plain data,
so a schema kept in a file, or the schemaString of a Delta table's
log, reads the same way with nothing installed. Membership is the
neutral checker's reading of the schema. Spark is not consulted, and
the record says the adaptor is spark.
What it reads¶
| Spark type | Neutral model | Notes |
|---|---|---|
ByteType, ShortType, IntegerType, LongType |
int8, int16, int32, int64 |
a value past the width is outside |
FloatType, DoubleType |
float32, float64 |
NaN is a member, as it is in Spark |
DecimalType(p, s) |
decimal(p, s) |
at most s places after the point and p - s digits before it; DecimalType() is decimal(10, 0) |
StringType, and CharType(n), VarcharType(n) |
string |
a CHAR or VARCHAR length is read from the field's metadata |
BooleanType, BinaryType, DateType |
bool, binary, date |
|
TimestampType, TimestampNTZType |
datetime |
naive, as collect() returns it |
ArrayType, MapType |
list, map |
containsNull and valueContainsNull say whether an item may be null |
a nested StructType |
a struct of its fields | a dict in a member |
nullable=True |
the field may hold None |
|
| any other type | any |
named in the schema the record shows |
A Spark schema states types and nullability, not ranges, so nothing in it bounds a price from below. A claim that needs a bound states it on a path.
A worked claim¶
A purchase order as the procurement pipeline reads it, from
examples/shop/procurement.py. The schema is the JSON the pipeline
passes to spark.read.schema(StructType.fromJson(PURCHASE_ORDER)):
PURCHASE_ORDER = {
"type": "struct",
"fields": [
{"name": "po_id", "type": "string", "nullable": False, "metadata": {}},
{"name": "quantity", "type": "integer", "nullable": False, "metadata": {}},
{"name": "unit_price", "type": "decimal(10,2)", "nullable": False, "metadata": {}},
{"name": "po_value", "type": "decimal(18,2)", "nullable": True, "metadata": {}},
],
}
def line_total(po: dict) -> Decimal:
"""What the order's lines come to."""
return po["quantity"] * po["unit_price"]
The same schema as a StructType reads identically:
StructType([
StructField("po_id", StringType(), False),
StructField("quantity", IntegerType(), False),
StructField("unit_price", DecimalType(10, 2), False),
StructField("po_value", DecimalType(18, 2), True),
])
f = line_total
for po in L[shop.procurement.PURCHASE_ORDER], po.quantity in [0, 100], po.unit_price in [0, 99.99], f(po) <= 9999
proven
for po in L[shop.procurement.PURCHASE_ORDER], f(po) >= 0
falsified po = {'po_id': '', 'quantity': 1, 'unit_price': Decimal('-0.01'), 'po_value': Decimal('0')}: Decimal('-0.01') vs 0
The first is proven from the bounds the claim puts on the two fields.
The second is falsified because the schema allows a negative price: a
decimal(10, 2) holds -0.01 as readily as 0.01.
Enforcing the schema¶
def spend_band(po: dict) -> str:
"""The spend band a purchase order is reported under."""
value = po["po_value"]
if value is None or value < 0:
return "Invalid"
if value < 1000:
return "Micro"
if value < 10000:
return "Standard"
if value < 50000:
return "Strategic"
return "Major"
f = spend_band
for po in L[shop.procurement.PURCHASE_ORDER], po.po_value in {absent}, f(po) == "Invalid"
holds
for po in L[shop.procurement.PURCHASE_ORDER], excluded_outside_domain(po)
falsified po = {'po_id': '', 'quantity': 0, 'unit_price': Decimal('0.001'), 'po_value': Decimal('0')} (outside L[shop.procurement.PURCHASE_ORDER] at .unit_price: decimal(10, 2): at most 2 places after the point…
A price of 0.001 has one place more than decimal(10, 2) holds, and
spend_band banded the order anyway. That is fine for a function that
only ever sees rows Spark has already read, and the claim is the one
to write for the function that reads the order in, before anything is
banded.