# CSV Join Guard — Duplicate Keys & Join Row Estimates (`brandgard/csv-join-guard`) Actor

Predict CSV join row counts before merging data. Detect duplicate keys, many-to-many expansion, unmatched rows and empty keys. Composite keys supported. JSON, CSV and HTML reports. $0.25 per completed check; fixed demo free.

- **URL**: https://apify.com/brandgard/csv-join-guard.md
- **Developed by:** [Brandon Gardner](https://apify.com/brandgard) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.25 / completed join check

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## CSV Join Guard

Check two CSV files before merging them. If a key appears twice on the left and three times on the right, an equality join creates six rows for that key. This Actor counts those combinations without building a potentially huge joined table.

Useful for data engineers, scraper users and automation builders preparing inventory, catalog or other tabular exports. It diagnoses join cardinality; it does not join, clean, modify or certify your data.

### Try the free sample

Run `{"demo":true}` or use the prefilled form. The optional unchecked `trimKeys:false` switch submitted by the form is also accepted; other custom fields are rejected. The fixed synthetic sample produces 5 inner-join rows, 7 left-join rows, 6 right-join rows and 8 full-join rows. Product A has a many-to-many match; an empty SKU and unmatched products are reported separately. The fixed sample cannot process custom data and triggers no priced event.

### Your own files

Disable **Run fixed demonstration** and paste two comma-separated UTF-8 CSV files with headers. Set `leftKeys` and optionally `rightKeys`; they default to `["id"]` and the left names respectively. Up to four key columns are matched by position, using exact case-sensitive text equality. Leading zeros are preserved.

```json
{
  "demo": false,
  "leftCsv": "order_id,sku\nO-1,A\nO-2,A\n",
  "rightCsv": "code,name\nA,Red\nA,Blue\n",
  "leftKeys": ["sku"],
  "rightKeys": ["code"],
  "expectedRelationship": "many_to_one"
}
```

This example predicts four inner-join rows and flags a violation of the requested many-to-one relationship. An optional expectation can be `one_to_one`, `one_to_many`, `many_to_one` or `many_to_many`. Expectations concern matched keys only; check unmatched and empty-key counts separately.

An empty component is treated as SQL NULL and never matches, including another empty component. Literal `NULL`, `null` and `N/A` are ordinary text. `trimKeys:true` removes outer whitespace before matching; false is the default. Numeric coercion, fuzzy matching, case folding, locale comparison and Unicode normalization are not performed. These semantics can differ from pandas, spreadsheet, database or other join settings; compare the rules before relying on the estimates.

### Outputs

The Output tab opens a readable HTML report. Storage → Key-value store contains `OUTPUT` (full JSON), `REPORT-CSV` (one row per non-empty key group), and `REPORT-HTML`. Download JSON/CSV there if your browser blocks an embedded view. The default dataset has a summary row so downstream automation can read counts easily. Full key-level evidence is in `OUTPUT`, not the summary dataset.

The report provides inner, left, right and full equality-join row estimates, unmatched row counts, empty-key counts, duplicate-key group counts, many-to-many groups and expectation violations. Each key group includes side counts, predicted inner output rows and up to five source record examples per side. Up to twenty empty-key record references are included per side. Counts are complete within accepted inputs; reference examples are truncated. Record numbers count non-blank data records after the header, not physical lines.

CSV key values are JSON-encoded arrays, preserving component boundaries and making formula-like key values start with a literal bracket. Other exported cells are protected against spreadsheet formulas. HTML escapes source text and contains no scripts or external resources. No other input columns are returned.

### Limits

- 1–5,000 non-blank data records and 2 MB UTF-8 per input.
- Comma-separated CSV only, including quoted fields and multiline records. No Excel, PDF, URLs or file download.
- 1–200 distinct, non-empty headers, at most 256 characters each. Selected key names must exist.
- Each field at most 16,384 characters; each key component at most 256 characters.
- Missing or extra CSV cells, malformed CSV, excessive limits or invalid configuration fail the run.
- Each export at most 8 MB; dense reports exceeding the limit fail before charging. Split your inputs when needed.
- No join table is materialized, even if predicted output is millions of rows.

### Pricing and privacy

**$0.25 per completed check**, platform usage included, with no separate startup or dataset-item fee. The Pricing tab is authoritative. One `check-completed` event covers both files within the limits, including no-match results or a report containing relationship violations. Structural failures and the fixed demo do not trigger that event. Set the run maximum cost to at least $0.25 for your own data.

Reports and summary are saved before charging. A retry within the same run uses a stable billing key; resurrecting can add another summary row but does not charge another completed-check event for the same run. A new run is a new purchase, even with unchanged input. There is no subscription or managed baseline.

Data stays in the caller's Apify run storage under its access and retention settings. The Actor does not automatically delete it. No data is sent to AI providers, websites or other external services. Only supply files you have permission to process. Avoid sensitive personal data when a small set of non-sensitive identifiers will do. No source rows or keys are logged by the Actor.

Run when a new export arrives, using a saved task or Apify API. The surrounding workflow supplies both files each time. Review the report before making downstream changes; inferred equality does not prove real-world identity or completeness.

For support, open an Actor issue with the run ID and a small synthetic or anonymized example. Never post private source data publicly.

# Actor input Schema

## `demo` (type: `boolean`):

Free synthetic sample only. Disable before adding custom fields.

## `leftCsv` (type: `string`):

Comma-separated UTF-8 text including header. Omit for fixed demo.

## `rightCsv` (type: `string`):

Second CSV, including header. Empty key components never match.

## `leftKeys` (type: `array`):

1–4 exact header names. Defaults to \["id"] when omitted.

## `rightKeys` (type: `array`):

Same number as left keys, paired by position. Defaults to left names.

## `trimKeys` (type: `boolean`):

False when omitted. If true, trims before grouping; whitespace-only values become empty keys.

## `expectedRelationship` (type: `string`):

Checks matched keys only. Omit if no expectation. No field defaults for the demo.

## Actor input object example

```json
{
  "demo": true
}
```

# Actor output Schema

## `html` (type: `string`):

No description

## `csv` (type: `string`):

No description

## `report` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "demo": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("brandgard/csv-join-guard").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "demo": True }

# Run the Actor and wait for it to finish
run = client.actor("brandgard/csv-join-guard").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "demo": true
}' |
apify call brandgard/csv-join-guard --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,brandgard/csv-join-guard"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Imi1WLP5qyyU4NUs1/builds/J7pJqBcPLwQziG0un/openapi.json
