Skip to main content

A CLI and library for analyzing FileMaker DDRs. Built for agents — JSON-first, scriptable, headless.

Project description

fmddr

A CLI and library for analyzing FileMaker DDRs (Database Design Reports).

fmddr parses a DDR XML once into a SQLite index, then answers structural and cross-reference queries in milliseconds. The output is JSON-first and stable, so an agent — or a script, or a developer at the terminal — can drive analysis at the same depth a GUI tool offers, without the GUI.

Install

Requires Python 3.11+.

pip install fmddr
# or for an isolated CLI install:
pipx install fmddr

fmddr --version
# fmddr 0.1.0

Listed on PyPI →

From source (for development)

git clone https://github.com/proofsh/fmddr.git
cd fmddr
pip install -e .[dev]    # adds pytest, ruff, build, twine
pip install -e .[lxml]   # adds libxml2-bound parser (optional)

Building a wheel locally

pip install build
python -m build
# → dist/fmddr-<version>-py3-none-any.whl

Quickstart

# 1. Explode the DDR into a per-object XML tree (grep/diff friendly).
fmddr split path/to/Profile.xml --out ./xmls-split

# 2. Build the SQLite index (the "ingest" step).
#    Works on a single DDR (`type="Report"`) or a `Summary.xml` that links
#    multiple files together — both produce one queryable index.
fmddr index path/to/Profile.xml --out ./profile.fmddr.db
fmddr index path/to/Summary.xml --out ./solution.fmddr.db

# 3. Query.
fmddr stats --db ./profile.fmddr.db
fmddr table fields "Production Run" -o table
fmddr field script-refs "Production Run::ProductionRunStatus"
fmddr script chain "Save Case Request" --depth 2 --format mermaid
fmddr cf called-by "FindWordPartsInText"
fmddr impact field "Customer::__UID"
fmddr unused --kind script
fmddr search "Customer*" --kind script

When stdout is a TTY, results render as Rich tables. Pipe to a file or another process and you get JSON. Override with -o {json,jsonl,table,markdown,csv,tsv}.

Why

A FileMaker file with thousands of fields, scripts, and layouts is hard to refactor safely. Every change requires structural reasoning:

  • What scripts touch this field, and at which step?
  • Which layouts show it, and which objects on those layouts?
  • What does this script call, and who calls it?
  • If this field is removed, what breaks?
  • Where is this custom function used?
  • Which steps run risky ExecuteSQL or Evaluate?
  • What's dead code? What's an orphan reference?

fmddr answers each of these in a one-line shell command with structured output, so an agent can run thousands of these queries during a refactor — programmatically, with stable output an LLM can parse.

Performance

On a 491 MB DDR from a long-running production file:

  • Index build: ~7 s (one-time, streams through the XML)
  • 257 tables / 10,403 fields / 1,877 scripts / 189,026 steps
  • 873 layouts / 70,564 layout refs / 3,083 TOs / 2,758 relationships
  • 123 custom functions / 503 value lists / 8,810 predicate refs
  • Queries return in <50 ms

Streaming iterparse + element-clearing keeps memory bounded regardless of DDR size. SQLite with journal_mode=MEMORY, synchronous=OFF during build, plus VACUUM + PRAGMA optimize at end.

Command reference

Ingest

Command Purpose
fmddr split <ddr> Per-object XML tree (grep/diff friendly)
fmddr index <ddr> / <Summary.xml> Build the SQLite index (single file or multi-file)
fmddr stats Counts and metadata recorded in the index
fmddr file list Files ingested into this index, with per-file counts
fmddr open Interactive sqlite3 shell against the index

Tables and fields

Command Purpose
fmddr table list All base tables with field counts
fmddr table show <name|id> Table metadata (records, calc/unstored counts)
fmddr table fields <name|id> [--type ...] [--unstored] Fields on a table, optionally filtered
fmddr field show <Table::Field|id> Field metadata (calc text, AutoEnter, storage flags)
fmddr field references <token> [--kind ...] All references, denormalized
fmddr field script-refs <token> One row per (script, step) that names the field
fmddr field on-layouts <token> Distinct layouts that show the field
fmddr field field-refs <token> Other fields whose calcs name this field
fmddr field where <token> Counts grouped by owner_kind

Scripts and steps

Command Purpose
fmddr script list [--folder ...] [--name ...] All scripts, optionally filtered
fmddr script show <token> Script header + step count
fmddr script steps <token> [--type ...] [--has ...] Steps; --has execute_sql|evaluate|get_script_parameter|get_script_result
fmddr script calls <token> Scripts called from this script
fmddr script called-by <token> Everything that calls this script
fmddr script fields <token> Distinct fields touched anywhere in this script
fmddr script elements <token> Every distinct (field, script, layout, CF) referenced
fmddr script calcs <token> Every step's text
fmddr script chain <token> --depth N --format mermaid|dot BFS call-chain diagram (Mermaid / Graphviz / JSON)
fmddr script grep <token> <regex> Regex search step text within a script
fmddr step show "<Script>:<index>" Full step detail (FMP step id, has_* flags, var name, text)
fmddr step refs "<Script>:<index>" All fields/scripts/layouts/CFs this single step references
fmddr risky-steps --has <flag> [--limit N] Cross-script audit of risky steps (execute_sql etc.)

Layouts

Command Purpose
fmddr layout list [--folder ...] All layouts
fmddr layout show <token> Layout header
fmddr layout objects <token> [--type ...] Objects on the layout (Field/Button/Portal/TabControl/...)
fmddr layout fields <token> Distinct fields shown on the layout
fmddr layout scripts <token> Scripts referenced (buttons + ScriptTriggers)

Custom functions

Command Purpose
fmddr cf list All custom functions
fmddr cf show <token> CF metadata + body
fmddr cf calls <token> Other CFs this CF calls
fmddr cf fields <token> Fields referenced inside this CF body
fmddr cf called-by <token> Everywhere this CF is used

Relationship graph

Command Purpose
fmddr graph relationships All relationships
fmddr graph tos [--base-table ...] Table occurrences, optionally filtered
fmddr graph predicates <relationship_id> Join predicates for a relationship
fmddr graph path <from-TO> <to-TO> [--max-hops N] Shortest TO→TO path through relationships

Higher-order

Command Purpose
fmddr impact field <token> What breaks if this field is removed
fmddr impact script <token> [--depth N] Transitive script callers
fmddr unused --kind <field|script|layout|custom_function|value_list|table_occurrence> Dead code candidates
fmddr orphans Refs whose target rows don't exist
fmddr cleanup-candidates Names matching debt patterns (BACKUP/COPY/OLD/TEMP/...)

Search

Command Purpose
fmddr search <fts5-query> [--kind ...] [--limit N] FTS5 over names of every kind

Clipboard (macOS only, v1)

Copy an object straight from the SQLite index (no split-tree required) and paste into FileMaker — or hand-edit a split-tree XML file first if you want the file-on-disk workflow.

Command Purpose
fmddr copy --script "<name>" Look up the script in the index and copy as <fmxmlsnippet>
fmddr copy --table "<name>" Same for a base table
fmddr copy --cf "<name>" Same for a custom function (per-CF; split-tree only emits a whole catalog)
fmddr copy --vl "<name>" Same for a value list (per-VL; split-tree only emits a whole catalog)
fmddr copy --script "<name>" --steps all|5-12 Copy script steps (all or a subset) as a ScriptStep — paste into a script
fmddr copy <split-xml-path> File-path mode: validate + wrap + push (split-tree workflow)
fmddr paste [--type Script|Table|...] Read FM clipboard XML and emit <fmxmlsnippet>
fmddr validate --script "<name>" (or --table, --cf, --vl) Run pre-clipboard guards against the indexed object
fmddr validate <split-xml-path> File-path mode: guards against a split-tree file
fmddr clipboard formats List FM-recognized clipboard formats currently present

Index-backed mode requires an index built with --keep-xml (the default since 0.9). Supports <Script>, <BaseTable>, <CustomFunction>, and <ValueList> roots in v1. Per-<Layout> copy is not implemented: FileMaker has no whole-Layout clipboard format (only LayoutObject for individual objects within a layout). See docs/guides/clipboard.

Global flags

--db <path>                   Override default DB location (also $FMDDR_DB)
-o, --output {json,jsonl,table,markdown,csv,tsv}   Force output format
--version                     Show version and exit

DB lookup order: --db flag → $FMDDR_DB → unique *.fmddr.db in CWD → error.

Exit codes

Code Meaning
0 OK
1 Not found (no field/script/layout matched)
2 Ambiguous (qualify with Table::Field or --by-id)
64 Bad usage
70 Internal error

Output envelope

Every command emits a stable JSON shape:

{
  "kind": "field.script-refs",
  "version": 1,
  "ddr": {"path": "...", "sha256": "...", "indexed_at": "..."},
  "query": {"field": {"id": 8712, "name": "ProductionRunStatus", "table": "Production Run"}},
  "result": [
    {
      "script_id": 1245, "script_name": "Save Case Request", "folder": "case-management",
      "step_index": 82, "fm_line": 83, "step_type_name": "Perform Script",
      "step_text": "Perform Script [ \"Send Alert Notification\" ... ]",
      "via_to_id": 207, "via_to_name": "Production Run"
    }
  ],
  "result_count": 1,
  "truncated": false
}

kind is the agent contract — agents can dispatch on it without parsing free text. version: 1 allows future schema bumps without breaking older callers.

Tests double as runnable examples

tests/test_demo_questions.py runs through 22 representative analyses against a synthetic mini DDR fixture. Run it with -s to see the formatted output for each one:

pytest tests/test_demo_questions.py -s

When this file passes, the library answers all of them.

Architecture

src/fmddr/
├── cli.py                 # Typer entry point (thin shell)
├── parser/
│   ├── stream.py          # UTF-16 → UTF-8 decode + iterparse driver
│   ├── split.py           # Per-object XML tree explosion
│   └── refs.py            # The uniform Chunk[FieldRef|CustomFunctionRef] walker
├── index/
│   ├── schema.sql         # Canonical SQLite DDL
│   └── build.py           # Streaming insert with executemany batches + staged ref resolution
├── query/
│   ├── tables.py          # base_table + field queries
│   ├── fields.py          # Field reference drills
│   ├── scripts.py         # script + step + steps/refs/calls/elements/calcs
│   ├── layouts.py         # layouts + objects + fields + scripts
│   ├── cf.py              # Custom function queries
│   ├── graph.py           # TOs + relationships + predicates + path
│   ├── impact.py          # impact / unused / orphans / script-chain
│   ├── search.py          # FTS5 + script grep
│   └── resolvers.py       # name|id → (kind, id) with ambiguity handling
├── format/
│   └── output.py          # Stable envelope + multi-format rendering
└── db.py                  # Connection helpers + meta envelope info

The reference graph is the heart: field_ref, script_ref, layout_ref, custom_function_ref, value_list_ref, table_ref — every "where used / depends on" question reduces to a one- or two-hop SQL query.

Docs site

A full Astro/Starlight docs site lives in docs/:

cd docs
npm install
npm run dev          # http://localhost:4321
npm run build        # static site → docs/dist/, deployable anywhere

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fmddr-0.9.0b4.tar.gz (578.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fmddr-0.9.0b4-py3-none-any.whl (240.2 kB view details)

Uploaded Python 3

File details

Details for the file fmddr-0.9.0b4.tar.gz.

File metadata

  • Download URL: fmddr-0.9.0b4.tar.gz
  • Upload date:
  • Size: 578.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for fmddr-0.9.0b4.tar.gz
Algorithm Hash digest
SHA256 d28ef634c7212e8dc5b5e43f3729ef05ef74926d10e72971eb845afb5ce24107
MD5 c4f3075aac31f2177dc32056009f945b
BLAKE2b-256 ea153b8993ae8816c88fe9c875b1dba8ff4d9650f976452263a5464e462c2dc6

See more details on using hashes here.

File details

Details for the file fmddr-0.9.0b4-py3-none-any.whl.

File metadata

  • Download URL: fmddr-0.9.0b4-py3-none-any.whl
  • Upload date:
  • Size: 240.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for fmddr-0.9.0b4-py3-none-any.whl
Algorithm Hash digest
SHA256 994a6adef869b432f2a2e35c1667020782fbadc487e0bdfe14b00ea870918389
MD5 407dc099f5f1994130aa1125c29fadf7
BLAKE2b-256 6f6d4ad7774464b74102a88c07e0584937ae6578a256f62561f467ce06d9a74d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page