Skip to content

feat: add datacompy compare CLI with multi-backend support - #534

Closed
fdosani wants to merge 50 commits into
capitalone:mainfrom
capitalone-contributions:datacompy-cli-feature
Closed

feat: add datacompy compare CLI with multi-backend support#534
fdosani wants to merge 50 commits into
capitalone:mainfrom
capitalone-contributions:datacompy-cli-feature

Conversation

@fdosani

@fdosani fdosani commented Jun 25, 2026

Copy link
Copy Markdown
Member

Implements the datacompy compare CLI (closes #530), enabling dataset comparisons from the shell and CI/CD pipelines without writing Python.

  • Adds datacompy/cli/ subpackage: parser.py, compare.py, backends.py, loaders.py, sessions.py, output.py, errors.py
  • Supports all four backends: polars (default), pandas, spark, snowflake
  • Loads CSV, Parquet, and JSON files; Snowflake can compare native table refs (db.schema.table) or stage local files to a temp table
  • Exit codes: 0 match, 1 mismatch/threshold violated, 2 error
  • --max-unequal-rows N for threshold-based CI assertions
  • --json for machine-readable output; --quiet for exit-code-only pipelines
  • --debug re-raises unexpected exceptions for bug reporting
  • Argument validation split between argparse type= callables (value rules) and _validate_arg_combinations (cross-argument rules)
  • Session lifecycle managed via contextlib.ExitStack — Spark and Snowflake sessions close on both success and failure
  • All optional backend imports are deferred so datacompy --version does not pay the pyspark/snowflake import cost

fdosani added 21 commits June 22, 2026 16:24
…s and improve error handling for missing Snowflake config
@fdosani
fdosani marked this pull request as ready for review June 25, 2026 15:47
@fdosani
fdosani force-pushed the datacompy-cli-feature branch from d0cb953 to da92835 Compare July 21, 2026 14:41
@fdosani
fdosani requested review from a team as code owners July 21, 2026 14:41
@fdosani
fdosani removed the request for review from gladysteh99 July 21, 2026 14:42

@rhaffar rhaffar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice feature Faisal - a few comments on my part.

This would be a bigger change than the ones left in my review, but any thoughts on making compare args as passthroughs that we don't explicitly define for the CLI? I like defining the args explicitly, but I'm thinking this will have to mean that any changes to the compare interfaces will have to be reflected here from now on as well. We'd probably still need to keep a few explicit args here for special cases if we were to consider that though (like the spark name and SF config stuff).

Comment thread datacompy/cli/backends.py Outdated
Comment on lines +107 to +131
if args.on_index:
return PandasCompare(
df1,
df2,
on_index=True,
abs_tol=args.abs_tol,
rel_tol=args.rel_tol,
df1_name=args.df1_name,
df2_name=args.df2_name,
ignore_spaces=args.ignore_spaces,
ignore_case=args.ignore_case,
cast_column_names_lower=args.cast_column_names_lower,
)
return PandasCompare(
df1,
df2,
join_columns=args.on,
abs_tol=args.abs_tol,
rel_tol=args.rel_tol,
df1_name=args.df1_name,
df2_name=args.df2_name,
ignore_spaces=args.ignore_spaces,
ignore_case=args.ignore_case,
cast_column_names_lower=args.cast_column_names_lower,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably keep this to a single return call by making on_index false if args.on isn't set.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I'll build the shared kwargs once and only add the join key that applies, so there's a single return PandasCompare(...):

Comment thread datacompy/cli/compare.py
Comment on lines +80 to +86
if args.on_index and args.on:
raise BadArgsError("--on and --on-index are mutually exclusive.")
if not args.on_index and not args.on:
raise BadArgsError(
"--on is required (or --on-index for the pandas backend). "
"Specify at least one join column with --on COL."
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now we do this check in the Pandas Comparator - if we just pass on_index and join_columns directly to the comparator, I think we could get rid of these checks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair observation that the rules overlap with PandasCompare. I'd lean toward keeping them at the CLI layer though, for a couple of reasons: the comparator raises a plain ValueError, which isn't a CLIError, so main.py would surface it as a raw traceback instead of a friendly message + exit code 2; and the CLI message references the actual flags (--on / --on-index) rather than the library kwarg names. The --on-index is only supported with --backend pandas check is also CLI-only. There's a small redundancy, but it buys clean CLI errors across all four backends (polars/spark get args.on or [], so they wouldn't hit the comparator guard at all). Happy to discuss further though.

Comment thread datacompy/cli/loaders.py Outdated
return _stage_file_to_snowflake(session, ref, fmt, csv_delimiter)


def _stage_file_to_snowflake(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the use case for comparing local files is very limited for Snowflake - that said Snowpark has a set of file readers that can be used to read files from a Snowflake internal stage. We could use that to read in Snowpark dataframes that we can pass directly to the Snowflake compare object?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed the local-file → Snowflake path is a narrow use case. On the Snowpark file readers: they're a nice fit when the data already lives on an internal/external stage, and since SnowflakeCompare accepts a Snowpark DataFrame directly we could pass one straight through. The wrinkle is that those readers read from a stage, not a local path, so for a genuinely local file we'd still need a PUT to a stage first, it relocates the upload step rather than removing it, so it's not a clear simplification over write_pandas. Two options: (a) keep write_pandas for local files and additionally accept @stage/... refs via the Snowpark reader, or (b) drop local-file support for Snowflake entirely and only accept table/stage refs. I lean toward (b) for the first release given how narrow the use case is, what's your preference?

Comment thread datacompy/cli/parser.py
Comment on lines +122 to +129
keys.add_argument(
"--on",
action="append",
dest="on",
default=None,
metavar="COL",
help="Join column name (required unless --on-index is used). Repeat for composite keys: --on id --on date.",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moreso on the side of personal preference, but could we have this just accept either a single or comma separated list of args?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, easy to support both. I'll keep action="append" and add a type= callable that splits each value on commas, then flatten, so --on id,date and --on id --on date (and a mix) all work. Only edge case is a column name that literally contains a comma, repeating --on stays as the escape hatch for that, and I'll note it in the help text.

fdosani and others added 6 commits July 29, 2026 10:20
feat: reject local files for Snowflake backend, support comma-separated --on, and tidy compare factories
feat: reject local files for Snowflake backend, support comma-separated --on, and tidy compare factories
@CLAassistant

CLAassistant commented Jul 29, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
2 out of 3 committers have signed the CLA.

✅ OSPO-CapitalOne
✅ fdosani
❌ Dosani, Faisal


Dosani, Faisal seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@fdosani

fdosani commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

Closing in favour of #544

@fdosani fdosani closed this Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Command Line Interface

4 participants