Misata
Description
Generate realistic multi-table test data with foreign keys that resolve and aggregates that reconcile, returned with an integrity check. Seeds Postgres, MySQL and SQLite from your own schema, or exports CSV/JSON/SQL/Parquet. Deterministic, so the same schema and seed give the…
About
Generate realistic multi-table test data with foreign keys that resolve and aggregates that reconcile, returned with an integrity check. Seeds Postgres, MySQL and SQLite from your own schema, or exports CSV/JSON/SQL/Parquet. Deterministic, so the same schema and seed give the same rows.
Details
- Author
- rasinmuhammed
- Categories
- Database, Other, Developer Tools
Jump to
Setup
Install Misata in your MCP client (Claude Desktop, Cursor, Windsurf, and others).
Repository: https://github.com/rasinmuhammed/misata
Follow the installation instructions in the repository README, then restart your MCP client.
Use Misata from Claude / Cursor / Windsurf (MCP)
Misata ships a built-in](https://github.com/rasinmuhammed/misata/blob/HEAD/mcpb/)Model Context Protocolserver with a clear division of labour:the AI agent designs the schema, Misata guarantees the math.Agents are good at knowing that a veterinary clinic needs aspeciescolumn; Misata is good at making 50 000 rows where every foreign key resolves, every roll-up reconciles to the cent, and the same seed reproduces byte-identical output. The primary tool,generate_from_schema, accepts the agent's schema dict and returns the dataplus an integrity proof: per-relationship orphan counts the agent can show you.
2. Add to Claude Desktop(~/Library/Application Support/Claude/claude_desktop_config.json):
{ "mcpServers": { "misata": { "command": "misata-mcp" } } }
"Generate a fintech dataset with 1 000 customers, payments, and a 2% fraud rate."
"Design a clinical-trials database (sites, patients, visits, adverse events) and generate 100k rows."
"I need SaaS data: MRR from $50k in January, doubled by December, with a Q3 slump."
The agent designs whatever tables the request needs (any domain; it isn't limited to Misata's built-ins), calls Misata, writes CSVs to disk, and reports back with previews and the verified integrity summary. See theMCP guidefor Cursor/Windsurf/Zed setup and all six available tools.
mcp-name: io.github.rasinmuhammed/misata
misata generate \ --story "Brazilian fintech with R$ payments, CPF verification, and 3% fraud" \ --rows 1000 \ --output-dir ./demo_data # Writes CSVs plus: # ./demo_data/oracle_report.json
import misata # One sentence → multi-table DataFrame dict tables = misata.generate("A SaaS company with 5k users, monthly subscriptions, and 20% churn") print(tables["users"].head()) print(tables["subscriptions"].head())
# Or from the CLI misata generate --story "A SaaS company with 5k users and 20% churn" --rows 5000
The Oracle report is Misata's proof layer. It separates hard guarantees from advisory realism checks so generated data can be trusted in CI, demos, notebooks, and research comparisons.
- referential integrity across configured relationships
- requested row-count fulfillment
- schema validation and configured constraints
- deterministic reproducibility when a seed is set
- quality score and plausibility warnings
- privacy heuristics
- schema-vs-output fidelity score
- locale/domain fit for countries, cities, phone prefixes, and national IDs
- data-card metadata
import misata schema = misata.parse("Brazilian fintech with CPF verification", rows=1000) tables = misata.generate_from_schema(schema) oracle = misata.build_oracle_report(tables, schema, seed=schema.seed) print(oracle["passed"]) print(oracle["advisory"]["locale_domain_fit"]["locale"])
Pointmisata.mimic()at a real dataset and get a synthetic twin that matches every column's distributions but contains none of the original rows. No schema authoring, no config.
import pandas as pd import misata real = pd.read_csv("titanic.csv") twin = misata.mimic(real, rows=2000, seed=42, table_name="passengers")["passengers"]
The profiler handles the columns that break other tools:
- Alphanumeric code columns(Ticket"A/5 21171", Cabin"C85", SKUs, reference numbers) are detected by their character-class shape and reproduced structurally, same shapes in the right proportions, entirely new values, zero verbatim leak from the source. They no longer fall through to prose text generation.
- Floats keep their cents.A Fare of7.25generates as7.25-shaped values. The profiler infers decimal places from the data; semantic quantization (charm pricing) never fires on mimicked columns.
- Distributions are fit from the data.Skewed-positive columns get lognormal; constant columns get a uniform stub; everything else gets normal. Categorical columns with fewer than 50 values carry their real frequencies.
# Verify: no verbatim rows can leak through shared = [c for c in real.columns if c in twin.columns] overlap = pd.merge(real[shared].astype(str), twin[shared].astype(str), how="inner") assert len(overlap) == 0
tables = misata.generate("A fintech startup with 10k customers, fraud rate 3%, and IBAN accounts")
Misata reads the story, infers domain (fintech), scale (10 000 rows), and column semantics (fraud flag, IBAN format), no schema authoring needed.
2. YAML schema-as-code, commit it to git
misata init # scaffolds misata.yaml in the current directory misata generate # reads misata.yaml automatically
# misata.yaml name: my-app seed: 42 tables: users: rows: 1000 columns: user_id: { type: int, unique: true } email: { type: text, text_type: email } plan: { type: categorical, choices: [free, pro, enterprise] } orders: rows: 5000 columns: order_id: { type: int, unique: true } user_id: { type: foreign_key } amount: { type: float, min: 5.0, max: 500.0 } relationships: - "users.user_id → orders.user_id" constraints: - name: amount_above_cost table: orders type: inequality column_a: amount operator: ">" column_b: cost
schema = misata.load_yaml_schema("misata.yaml") tables = misata.generate_from_schema(schema)
from misata import schema_from_db, generate_from_schema, seed_database # Introspect the live schema: no manual column definitions schema = schema_from_db("postgresql://user:pass@localhost/myapp") tables = generate_from_schema(schema) # Seed it back: insert order respects FK dependencies automatically report = seed_database(tables, "postgresql://user:pass@localhost/myapp_dev") # SeedReport: seeded 6 tables, 47,300 rows in 1.2s
# One-command workflow misata init --db postgresql://user:pass@localhost/myapp # writes misata.yaml misata generate --db-url postgresql://user:pass@localhost/myapp_dev --db-create
from misata import seed_from_sqlalchemy_models from myapp.models import Base report = seed_from_sqlalchemy_models(Base, db_url="sqlite:///test.db", row_count=500, create_tables=True)
4. From a dbt project's own schema.yml
cd my-dbt-project && misata dbt-seed
No story, no config. Misata reads the properties YAML your project already has and generates seed CSVs that satisfy it:relationshipstests become foreign keys with guaranteed integrity,accepted_valuesbecome the exact category pools,uniqueandnot_nullbecome hard constraints, anddata_typeplus column-name semantics decide the rest. Then:
dbt build # seed + run + test — the tests you already wrote, passing on day zero
Both the legacy inline test syntax and the dbt 1.9+arguments:nesting are understood. Tests Misata can't translate (dbt_utils.*, custom generics) are listed in the output rather than silently guessed at.
Reads the schema.prisma your app already maintains:@relationbecomes foreign keys with zero orphans, enums become the exact value pools,@idand@uniqueare honoured,@@id/@@uniquebecome composite uniqueness, and optional fields may be null. CSVs land inseed-data/ready for your seed script.
schema = misata.from_dict_schema({ "customers": { "id": {"type": "integer", "primary_key": True}, "email": {"type": "email"}, "plan": {"type": "string", "enum": ["free", "pro", "enterprise"]}, }, "orders": { "id": {"type": "integer", "primary_key": True}, "customer_id": {"type": "integer", "foreign_key": {"table": "customers", "column": "id"}}, "amount": {"type": "float", "min": 1.0, "max": 999.0}, "order_date": {"type": "date"}, }, }, row_count=5_000) tables = misata.generate_from_schema(schema)
Declared outcome curves: add__outcome_curves__as a top-level key alongside the table definitions. Generated rows sum to every declared target exactly, to the cent:
import pandas as pd schema = misata.from_dict_schema({ "__outcome_curves__": [{ "table": "orders", "column": "amount", "time_column": "order_date", "time_unit": "month", "value_mode": "absolute", "start_date": "2024-01-01", "avg_transaction_value": 120.0, "curve_points": [ {"month": 1, "target_value": 50_000.0}, {"month": 6, "target_value": 110_000.0}, {"month": 12, "target_value": 200_000.0}, ], }], "orders": { "__rows__": 5000, "order_id": {"type": "integer", "primary_key": True}, "amount": {"type": "float", "min": 5, "max": 500}, "order_date": {"type": "date"}, }, }, seed=42) tables = misata.generate_from_schema(schema) monthly = ( tables["orders"] .assign(m=pd.to_datetime(tables["orders"]["order_date"]).dt.month) .groupby("m")["amount"].sum() ) assert abs(monthly[1] - 50_000) < 0.01 # exact assert abs(monthly[12] - 200_000) < 0.01 # exact
Exact group shares: declare how a measure divides across a categorical column ("Electronics is 40% of revenue, Home 25%") with__group_shares__. Paired with an outcome curve on the same table and measure, the shares hold to the cent inside every declared period, and the period totals still hold; without a curve, the shares hold over the table total:
schema = misata.from_dict_schema({ "__group_shares__": [{ "table": "orders", "measure": "amount", "group_column": "category", "shares": {"Electronics": 0.4, "Home": 0.25, "Toys": 0.2, "Grocery": 0.15}, }], # ... same orders table and __outcome_curves__ as above, # plus a "category" enum column }, seed=42)
A period with fewer rows than positive-share groups is skipped with a warning rather than silently mangled; see LIMITATIONS.md.story_auditverifies the shares in the output, and evalpacks turn each period-group pair into a verified filtered-aggregation question.
Constraints and correlations: enforce business rules and inter-column relationships directly in the dict schema:
schema = misata.from_dict_schema({ "patients": { "__rows__": 1000, "__constraints__": [ # visit must be on or after enrollment: enforced at generation, not post-processing {"type": "inequality", "column_a": "visit_date", "operator": ">=", "column_b": "enroll_date", "action": "cap"}, ], "__correlations__": [ # heavier patients tend to have higher blood pressure (r = 0.41) {"col_a": "bmi", "col_b": "systolic_bp", "r": 0.41}, ], "patient_id": {"type": "integer", "primary_key": True}, "enroll_date": {"type": "date"}, "visit_date": {"type": "date"}, "bmi": {"type": "float", "min": 16, "max": 55}, "systolic_bp": {"type": "float", "min": 90, "max": 200}, }, })
__rate_curves__works the same way for per-period rate targets on boolean or categorical columns (fraud rates, churn flags, plan distributions).
7. LLM-assisted generation, richer semantics, optional
from misata import LLMSchemaGenerator gen = LLMSchemaGenerator(provider="groq", model="llama-3.3-70b-versatile") # free tier, fast & reliable # gen = LLMSchemaGenerator(provider="anthropic") # Claude # gen = LLMSchemaGenerator(provider="ollama", model="llama3") # fully local, no API key schema = gen.generate_from_story( "A fraud detection dataset, 2% positive rate, FICO scores, transaction velocity features" ) tables = misata.generate_from_schema(schema)
Requirespip install "misata[llm]"plus one ofGROQ_API_KEY,OPENAI_API_KEY,ANTHROPIC_API_KEY,GOOGLE_API_KEY.
Groq model tip:llama-3.3-70b-versatileis the reliable free-tier default. Larger models (e.g.openai/gpt-oss-120b) can return413 Request too largeon Groq's free tier, so use them only on a paid tier. Whatever the model returns, generation never crashes on an imperfect schema: missing relationships, malformed probabilities, and out-of-rangetime_units are repaired automatically.
8. Incremental generation, grow a dataset without re-seeding
tables = misata.generate("A fintech company with 1000 customers", seed=1) # Add 1 000 more rows: IDs auto-offset, FK integrity maintained across both batches tables = misata.generate_more(tables, schema, n=1000, seed=2) print(len(tables["customers"])) # 2000
Synthetic data rarely fails on the big numbers; it fails on the small tells a reviewer spots in five seconds. Misata kills each tell with a specific, deterministic mechanism. No LLM is involved; everything is seeded and reproducible.
tables = misata.generate("A hospital with 300 patients, doctors and appointments", seed=7) # patients: Tae-yang Ahn (Male) · Valentina Esposito (Female) · pooja.kapoor@icloud.com # appointments: 2023-03-08 14:00:00 · 2022-07-21 09:15:00: 15-min grid, business hours, 2% weekends
Every coherence class above is also a detector.story_auditchecks a generated dataset against the full invariant catalog: FK orphans, cross-table temporal causality, roll-up agreement, status gating, count and percent bounds, rare-flag base rates, age against birth date, and more. Nothing incoherent ships silently.
tables = misata.generate_from_schema(schema, verify=True) # warns on any finding report = misata.story_audit(tables, schema) # or audit explicitly print(report.summary()) # "Coherence: clean" or a scored list of findings
Every evalpack manifest embeds this verdict alongside its DuckDB answer certificate, so a pack asserts both that its answers are right and that the data telling the story is internally coherent.
- Within a version, generation is deterministic.The same schema, seed, and misata version produce byte-identical tables. Evalpack manifests record the version, seed, and a SHA-256 of the spec for exactly this reason.
- Across versions, RNG streams may changewhen generation improves (they did in 0.8.1.29 and 0.8.2). Declared outcomes still hold: aggregates, rates, identities, and integrity survive any upgrade; the individual rows may differ. Pin the version when you need bit-identical regeneration.
- The public API is the documented top-level surface(misata.generate,generate_from_schema,story_audit,coherence_audit,build_evalpack, the schema classes, and the builders). Underscore-prefixed modules and functions may change without notice.
Where the library is expected to fail is documented honestly, boundary by boundary, in[LIMITATIONS.md. Every entry there started as a reproduced defect or a deliberate design refusal.
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.





