Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ae21179
Replace community.general.npm with ansible.builtin.command
claude Mar 1, 2026
9b88edd
Add CI workflow to validate Ansible playbook on Ubuntu
claude Mar 2, 2026
a7f5784
Add dbt keybindings and SQL treesitter support
claude Mar 5, 2026
ab536f3
Add more dbt keybindings: build, show, run+downstream
claude Mar 5, 2026
b6171e3
Add dbt navigation, fuzzy model picker, and model search
claude Mar 5, 2026
93d5ca4
Add claude agent keymaps for dbt model analysis
claude Mar 5, 2026
45dcc02
Include compiled SQL and sample rows in deep dbt analysis
claude Mar 5, 2026
70af540
Move dbt agent prompts to separate markdown template files
claude Mar 5, 2026
25dc51a
Write inline comments to buffer and prefix all dbt commands with uv run
claude Mar 5, 2026
c7d612f
Open deep dbt analysis in interactive tmux window
claude Mar 5, 2026
2038e17
Add <leader>dp to preview dbt model rows in a split
claude Mar 5, 2026
0885d15
Return cursor to code buffer after dbt_cmd sends to toggleterm
claude Mar 5, 2026
ea2df0d
Replace fzf-lua with telescope.builtin across all keymaps
claude Mar 5, 2026
35131c3
Add <leader>dv to show dbt model output in visidata
claude Mar 5, 2026
4983db0
Return focus to code buffer after visidata exits
claude Mar 5, 2026
ed173af
Fix dbt visidata: use json output with json-to-csv conversion
claude Mar 5, 2026
b4fe1b8
Rename dbt prompt files
michaelbarton Mar 6, 2026
054a855
Add explicit python scripts for db analsis
michaelbarton Mar 6, 2026
b5a745b
Update dbt nvim keymaps
michaelbarton Mar 6, 2026
abd77b0
Fix resource leaks, safety, and robustness issues in dbt audit scripts
claude Mar 6, 2026
bd95949
Enrich dbt audit with lineage, test coverage, and structured prompts
claude Mar 6, 2026
0c6912b
Clean up dead code in dbt_analyse.py
claude Mar 6, 2026
9eb9e43
Split dbt nvim keymaps into their own file
claude Mar 6, 2026
f31143b
Add full spec for data-audit Python package
claude Mar 8, 2026
9b6993f
Merge branch 'master' into claude/fix-status-checks-0nymq
claude Mar 10, 2026
01a367b
Fix black formatting in dbt Python scripts
claude Mar 10, 2026
7367bf9
Merge remote-tracking branch 'origin/master' into claude/fix-status-c…
claude Mar 10, 2026
e1269e9
Delete dbt/SPEC.md
michaelbarton Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions ansible/tasks/neovim.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
- ruff
- yamllint

- name: Remove existing ftplugin directory if it exists (to allow symlinking)
- name: Remove existing directories that need to be replaced with symlinks
ansible.builtin.file:
path: "{{ ansible_env.HOME }}/.config/nvim/ftplugin"
path: "{{ ansible_env.HOME }}/.config/nvim/{{ item }}"
state: absent
loop:
- ftplugin
- dbt

- name: Link specific configuration files

ansible.builtin.file:
src: "{{ playbook_dir }}/../{{ item.src }}"
dest: "{{ ansible_env.HOME }}/{{ item.dest }}"
Expand All @@ -30,6 +32,7 @@
- { src: "nvim/init.lua", dest: ".config/nvim/init.lua" }
- { src: "nvim/lua", dest: ".config/nvim/lua" }
- { src: "nvim/ftplugin", dest: ".config/nvim/ftplugin" }
- { src: "dbt", dest: ".config/nvim/dbt" }

- name: Install jsonlint node.js package
ansible.builtin.command:
Expand Down
228 changes: 228 additions & 0 deletions dbt/dbt_analyse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["click", "pyyaml"]
# ///
"""
dbt_analyse: compile a dbt model, gather context, then launch an interactive
cursor-agent session. Designed to be called from a tmux window.

Usage:
dbt_analyse.py --model <name> --root <dbt_project_root> \
--filepath <source_sql_path> --prompt <prompt_template_path>
"""

import subprocess
import sys
import glob
import os
import re
import tempfile

import click
import yaml


def run(cmd, cwd=None, capture=False, check=True):
result = subprocess.run(
cmd,
cwd=cwd,
capture_output=capture,
text=True,
)
if check and result.returncode != 0:
stderr = result.stderr.strip() if result.stderr else ""
click.echo(f"ERROR: command failed (exit {result.returncode}): {' '.join(cmd)}", err=True)
if stderr:
click.echo(stderr, err=True)
sys.exit(result.returncode)
return result


def get_lineage(model, root):
"""Return a summary of immediate parents and children from dbt ls."""
lines = []
for direction, selector in [
("parents", f"+{model},1+{model}"),
("children", f"{model}+,{model}1+"),
]:
result = subprocess.run(
["uv", "run", "dbt", "ls", "-s", selector, "--output", "name", "--quiet"],
capture_output=True,
text=True,
cwd=root,
)
if result.returncode == 0:
names = [
n.strip()
for n in result.stdout.strip().splitlines()
if n.strip() and n.strip() != model
]
if names:
lines.append(f"**{direction.title()}:** {', '.join(names)}")
return "\n".join(lines) if lines else ""


def get_existing_tests(model, root):
"""Extract test definitions for a model from schema.yml files."""
unique_files = glob.glob(os.path.join(root, "**", "*.yml"), recursive=True)

tests = []
for schema_path in unique_files:
try:
with open(schema_path) as f:
doc = yaml.safe_load(f)
except (yaml.YAMLError, OSError):
continue
if not isinstance(doc, dict):
continue
for m in doc.get("models", []):
if not isinstance(m, dict) or m.get("name") != model:
continue
# Model-level tests
for t in m.get("tests", []):
tests.append(f"- model-level: {t}")
# Column-level tests
for col in m.get("columns", []):
if not isinstance(col, dict):
continue
col_name = col.get("name", "?")
for t in col.get("tests", []):
if isinstance(t, str):
tests.append(f"- {col_name}: {t}")
elif isinstance(t, dict):
tests.append(f"- {col_name}: {t}")
return "\n".join(tests) if tests else ""


def render_template(template, replacements):
"""Replace template placeholders, handling conditional {{#if}}/{{^if}} blocks."""
for key, value in replacements.items():
# Handle {{#if key}}...{{/if}} blocks
if_pattern = re.compile(
r"\{\{#if " + re.escape(key) + r"\}\}(.*?)\{\{/if\}\}",
re.DOTALL,
)
not_pattern = re.compile(
r"\{\{\^if " + re.escape(key) + r"\}\}(.*?)\{\{/if\}\}",
re.DOTALL,
)
if value:
template = if_pattern.sub(r"\1", template)
template = not_pattern.sub("", template)
else:
template = if_pattern.sub("", template)
template = not_pattern.sub(r"\1", template)
# Replace the simple placeholder
template = template.replace("{{" + key + "}}", value)
return template


@click.command()
@click.option("--model", required=True, help="dbt model name (no extension)")
@click.option("--root", required=True, help="Path to dbt project root")
@click.option("--filepath", required=True, help="Absolute path to the source SQL file")
@click.option("--prompt", required=True, help="Path to the prompt template .md file")
@click.option("--limit", default=20, show_default=True, help="Row limit for dbt show")
@click.option(
"--model-flag", default="sonnet-4.6-thinking", show_default=True, help="cursor-agent model"
)
def main(model, root, filepath, prompt, limit, model_flag):
# --- 1. compile ---
click.echo(f"Compiling {model}...")
run(["uv", "run", "dbt", "compile", "-s", model, "--quiet"], cwd=root)

# --- 2. find compiled SQL ---
pattern = os.path.join(root, "target", "compiled", "**", f"{model}.sql")
matches = glob.glob(pattern, recursive=True)
if not matches:
click.echo(f"ERROR: no compiled SQL found for {model} — did compile succeed?", err=True)
sys.exit(1)
with open(matches[0]) as f:
compiled_sql = f.read()
click.echo(f"Compiled SQL: {matches[0]}")

# --- 3. sample rows ---
click.echo(f"Fetching sample rows (limit={limit})...")
result = run(
[
"uv",
"run",
"dbt",
"show",
"-s",
model,
"--limit",
str(limit),
"--output",
"json",
"--log-format",
"json",
],
cwd=root,
capture=True,
check=False,
)
if result.returncode != 0:
click.echo(
f"WARNING: dbt show failed (exit {result.returncode}), continuing without sample rows",
err=True,
)
sample_rows = "(dbt show failed)"
else:
sample_rows = result.stdout.strip() or "(no rows returned)"

# --- 4. source SQL ---
if not os.path.exists(filepath):
click.echo(f"ERROR: source file not found: {filepath}", err=True)
sys.exit(1)
with open(filepath) as f:
source_sql = f.read()

# --- 5. gather lineage and existing tests ---
click.echo("Gathering model lineage...")
lineage = get_lineage(model, root)

click.echo("Scanning for existing dbt tests...")
existing_tests = get_existing_tests(model, root)

# --- 6. build prompt ---
if not os.path.exists(prompt):
click.echo(f"ERROR: prompt template not found: {prompt}", err=True)
sys.exit(1)
with open(prompt) as f:
template = f.read()

full_prompt = render_template(
template,
{
"compiled_sql": compiled_sql,
"sample_rows": sample_rows,
"existing_tests": existing_tests,
"lineage": lineage,
"data_profile": "",
},
)
full_prompt += f"\n\nSource SQL:\n{source_sql}"

# --- 7. write context to a temp file & launch cursor-agent ---
ctx = tempfile.NamedTemporaryFile(
mode="w",
suffix=".md",
prefix=f"dbt_audit_{model}_",
delete=False,
)
ctx.write(full_prompt)
ctx.close()
click.echo(f"Context written to {ctx.name}")

click.echo(f"Launching cursor-agent ({model_flag})...")
agent_prompt = (
f"Read the audit instructions and dbt model context from {ctx.name}. "
"Perform a thorough data quality audit of the dbt model as described."
)
os.execlp("cursor-agent", "cursor-agent", "--model", model_flag, agent_prompt)


if __name__ == "__main__":
main()
Loading
Loading