# Databricks & Apache Spark The `misata.spark` module writes Misata-generated data straight into **Delta Lake** and imports existing Spark schemas back into Misata. It runs on **Databricks** (Free Edition or full), **AWS Glue**, **EMR**, **Dataproc**, or any local **PySpark 3.3+** environment. Misata's value on Spark over the usual `dbldatagen`: - **Multiple related tables in one call**: `dbldatagen` builds one table at a time with no awareness of relationships. - **Referential integrity guaranteed and verifiable**: zero orphan foreign keys, checked in Delta with anti-joins. - **Realistic distributions**: lognormal, Poisson, normal, uniform, and correlated columns. - **Outcome conformance**: declare an exact aggregate or rate (e.g. a monthly fraud rate) and the generated data conforms, giving pipeline tests a **known ground truth to assert against**. --- ## Installation === "Databricks serverless / Free Edition" Install **plain `misata`**: *not* `misata[spark]`. PySpark is already on every Databricks cluster, and installing it on a serverless notebook will stop the session. The module imports the cluster's PySpark lazily. ```python %pip install misata dbutils.library.restartPython() ``` === "Local / Glue / EMR / self-managed Spark" ```bash pip install misata[spark] # pulls pyspark>=3.3.0 ``` The module never imports PySpark at module-load time, so `import misata` stays lightweight. Every function calls `import pyspark` lazily and raises a clear error if it is missing. --- ## Quick start ```python import misata from misata import spark as mspark schema = misata.from_dict_schema({ "customers": { "__rows__": 500, "id": {"type": "integer", "primary_key": True}, "email": {"type": "email"}, "country": {"type": "string", "text_type": "country"}, }, "orders": { "__rows__": 2000, "id": {"type": "integer", "primary_key": True}, "customer_id": {"type": "integer", "foreign_key": {"table": "customers", "column": "id"}}, "total": {"type": "float", "distribution": "lognormal", "mu": 4.5, "sigma": 0.9, "min": 4.99}, }, }) result = mspark.generate_to_delta(schema, spark, catalog="dev", database="bronze", mode="overwrite") print(result.summary()) result.raise_on_error() ``` `spark` is the active `SparkSession`, pre-defined in every Databricks notebook. --- ## API reference ### `to_spark(tables, spark, *, infer_schema=False, date_columns=None, schema_config=None)` Convert Misata's `{table_name: pd.DataFrame}` output to `{table_name: SparkDataFrame}` using an **explicit, type-correct schema**. Spark's own inference can widen nullable integers to doubles or reject `datetime.date` objects; an explicit schema avoids both. ```python tables = misata.generate_from_schema(schema) spark_tables = mspark.to_spark(tables, spark, schema_config=schema) spark_tables["orders"].printSchema() ``` Pass `schema_config` so columns declared `type: "date"` are mapped to Delta `DateType` (rather than `TimestampType`). Misata stores both `date` and `datetime` as pandas `datetime64[ns]`, so the schema is the only way to recover the distinction. ### `write_delta(tables, spark, *, catalog=None, database=None, schema=None, mode="overwrite", partition_by=None, cluster_by=None, merge_keys=None, table_properties=None, optimize_after_write=False, create_schema_if_not_exists=True, date_columns=None, schema_config=None, verbose=True)` Write every table to Delta Lake. Returns a [`DeltaWriteResult`](#deltawriteresult). ```python mspark.write_delta( tables, spark, catalog="dev", database="bronze", mode="overwrite", partition_by={"events": ["event_date"]}, cluster_by={"transactions": ["txn_month"]}, # Delta liquid clustering table_properties={"delta.autoOptimize.optimizeWrite": "true"}, optimize_after_write=True, schema_config=schema, # preserve date typing ) ``` **Naming.** `catalog`, `database`/`schema`, and the table name assemble into Unity Catalog 3-part names (`catalog.database.table`), 2-part (`database.table`), or bare table names, whatever you provide. **Write modes:** | `mode` | Behaviour | |--------|-----------| | `"overwrite"` | Replace the table; schema changes are applied (`overwriteSchema`). | | `"append"` | Add rows to an existing table. | | `"merge"` | Idempotent **`MERGE INTO`** upsert keyed on `merge_keys`: for CDC / SCD pipeline testing. Requires `merge_keys={"table": ["id"]}`. | | `"error"` | Fail if the table already exists. | ```python # Upsert: re-running this updates matched rows instead of duplicating them mspark.write_delta(tables, spark, database="bronze", mode="merge", merge_keys={"customers": ["id"]}) ``` `cluster_by` uses Delta's `clusterBy` writer when available and **falls back gracefully** (writing without clustering, with a warning) on older Delta builds. ### `verify_delta_integrity(spark, relationships, *, catalog=None, database=None, schema=None, sample_size=5)` Verify referential integrity of Delta tables with a Spark SQL `LEFT ANTI JOIN` per relationship. Returns a [`SparkIntegrityReport`](#sparkintegrityreport). ```python relationships = [ {"from_table": "orders", "from_column": "customer_id", "to_table": "customers", "to_column": "id"}, {"from_table": "order_items", "from_column": "order_id", "to_table": "orders", "to_column": "id"}, ] report = mspark.verify_delta_integrity(spark, relationships, catalog="dev", database="bronze") print(report.summary()) report.raise_if_invalid() # raises ValueError listing orphan counts + samples ``` Shorthand dot-notation is also accepted: `{"from": "orders.customer_id", "to": "customers.id"}`. ### `from_spark_schema(source, spark=None, *, table_name="table", row_count=1000, foreign_keys=None, seed=42)` Convert a single Spark schema into a Misata `SchemaConfig`. `source` may be a `StructType`, a `DataFrame`, or a fully-qualified table-name string. Reads only the public `.schema`, so it works on Spark Connect / serverless. ```python schema = mspark.from_spark_schema( spark.table("prod.bronze.orders"), row_count=5000, foreign_keys={"customer_id": {"table": "customers", "column": "id"}}, ) ``` ### `from_catalog_table(table_name, spark, *, row_count=1000, foreign_keys=None, seed=42)` Import one Unity Catalog / Hive table by name. ```python schema = mspark.from_catalog_table("dev.bronze.orders", spark, row_count=5000) ``` ### `from_catalog_schema(spark, database, *, catalog=None, row_counts=None, foreign_keys=None, infer_foreign_keys=True, seed=42)` Import **every table** in a database and assemble a multi-table `SchemaConfig`. FK relationships are inferred from `{parent}_id` column naming (de-pluralisation aware: `order_id` → `orders.id`). Unmapped `*_id` columns produce a warning rather than a silent miss. ```python schema = mspark.from_catalog_schema( spark, database="bronze", catalog="prod", row_counts={"customers": 500, "orders": 2000, "order_items": 6000}, foreign_keys={ # override / supplement inference "order_items": {"sku_ref": {"table": "products", "column": "sku"}}, }, ) tables = misata.generate_from_schema(schema) ``` Use this to **mirror a production schema into a dev environment**: structure only, no data copied, and generate matching synthetic data. ### `append_to_delta(schema_config, spark, n_rows, *, catalog=None, database=None, schema=None, seed=None, verbose=True)` Generate additional rows and append them to existing Delta tables. Reads each table's current `MAX(id)` to offset new primary keys so they never collide. FK integrity is maintained within the new batch. Date typing conforms to the existing target table. ```python mspark.append_to_delta(schema, spark, n_rows={"customers": 200, "orders": 800}, database="bronze") ``` ### `write_delta_stream(schema_config, spark, *, catalog=None, database=None, schema=None, batch_size=100_000, partition_by=None, table_properties=None, optimize_after_write=False, create_schema_if_not_exists=True, verbose=True)` Stream-write very large datasets. Uses Misata's batch generator so the full dataset is never buffered in memory, suitable for tens or hundreds of millions of rows. ```python schema.tables[0].row_count = 50_000_000 mspark.write_delta_stream(schema, spark, database="bronze", batch_size=250_000) ``` ### `generate_to_spark(schema_or_story, spark, *, rows=10_000, seed=None, smart_correlations=False)` One-liner: generate and return Spark DataFrames. Accepts a `SchemaConfig` or a plain-English story string. ```python spark_tables = mspark.generate_to_spark("An ecommerce store with 5k orders", spark) ``` ### `generate_to_delta(schema_or_story, spark, *, catalog=None, database=None, schema=None, rows=10_000, seed=None, mode="overwrite", partition_by=None, cluster_by=None, table_properties=None, optimize_after_write=False, create_schema_if_not_exists=True, smart_correlations=False, verbose=True)` One-liner: generate and write to Delta. Accepts a `SchemaConfig` or a story string. --- ## Result types ### `DeltaWriteResult` Returned by `write_delta`, `write_delta_stream`, `append_to_delta`, `generate_to_delta`. | Member | Description | |--------|-------------| | `.table_paths` | `{table: fully-qualified Delta path}` | | `.rows_written` | `{table: row count}` | | `.errors` | `{table: error message}`: empty on full success | | `.ok` | `True` if no errors | | `.summary()` | Human-readable per-table report | | `.raise_on_error()` | Raise `RuntimeError` if any table failed | ### `SparkIntegrityReport` Returned by `verify_delta_integrity`. | Member | Description | |--------|-------------| | `.violations` | List of `SparkIntegrityViolation` | | `.ok` | `True` if no violations | | `.summary()` | Human-readable report | | `.raise_if_invalid()` | Raise `ValueError` listing all violations | Each `SparkIntegrityViolation` carries `child_table`, `child_column`, `parent_table`, `parent_column`, `orphan_count`, and `sample_orphan_values`. --- ## End-to-end tutorial [`examples/databricks/medallion_fraud_pipeline.py`](https://github.com/rasinmuhammed/misata/blob/main/examples/databricks/medallion_fraud_pipeline.py) builds a complete fraud-detection **medallion pipeline**: 1. **Bronze**: generate four FK-linked tables (`customers`, `accounts`, `merchants`, `transactions`) with a declared monthly fraud-rate curve (1.8% → 4.1%). 2. **Silver**: join all four tables, derive fraud-signal features. 3. **Gold**: aggregate monthly fraud rate and dollar volume. 4. **Assert** the Gold output matches the *declared* fraud rate, a CI-grade correctness test that's impossible with Faker or dbldatagen, because they give you no ground truth to check. Import the `.py` file into Databricks (**Workspace → Import → File**) and **Run all** on serverless. --- ## Compatibility | | | |---|---| | Spark | 3.3+ (verified on 3.5.3) | | Delta Lake | 2.x / 3.x (verified on 3.2.1) | | Spark Connect / serverless | ✅ no `sparkContext`, `_jdf`, or RDD access | | Unity Catalog | ✅ 3-part naming, managed tables | | JDK | 8 / 11 / 17 (verified on 17) | The full integration is covered by `tests/test_spark.py`, guarded by `pytest.importorskip("pyspark")` so it runs in CI/Databricks and skips cleanly elsewhere.