›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-data-sync
2.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start
  • Upgrading from 1.0

Core concepts

  • File identity and runs
  • Sync definitions
  • Sources and connections
  • File formats and readers
  • Field mapping
  • Matching and writing
  • Validation and hooks

Running syncs

  • Running a sync
  • Queues and workers
  • Scheduling

APIs and databases

  • Syncing from an API
  • Syncing from a database
  • Definition providers

Transfers

  • File transfers

Operations

  • Run history and status
  • Events and listeners
  • Failures and retries
  • Sensitive data
  • Health checks
  • Retention and pruning

Reference

  • Configuration
  • Console commands
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start
  • Upgrading from 1.0

Core concepts

  • File identity and runs
  • Sync definitions
  • Sources and connections
  • File formats and readers
  • Field mapping
  • Matching and writing
  • Validation and hooks

Running syncs

  • Running a sync
  • Queues and workers
  • Scheduling

APIs and databases

  • Syncing from an API
  • Syncing from a database
  • Definition providers

Transfers

  • File transfers

Operations

  • Run history and status
  • Events and listeners
  • Failures and retries
  • Sensitive data
  • Health checks
  • Retention and pruning

Reference

  • Configuration
  • Console commands
  • Testing
  • Troubleshooting

byrcsc/laravel-data-sync · 2.x

Troubleshooting.

Diagnose missing files, failed rows, stalled runs, and growing storage.

Catching package exceptions

Every exception the package throws extends ByRcsc\LaravelDataSync\Exceptions\DataSyncException, which extends RuntimeException. One catch covers the lot:

use ByRcsc\LaravelDataSync\Exceptions\DataSyncException;

try {
    DataSync::run('products');
} catch (DataSyncException $exception) {
    // every failure below, and nothing from outside the package
}
ExceptionThrown when
InvalidConfigurationExceptionconfig/data-sync.php is wrong, at boot, before anything runs
InvalidDefinitionExceptionA definition fails validation, or a name is not registered
SchemaMismatchExceptionA file is missing required columns, or carries unknown ones under Strict
DuplicateRowExceptionTwo rows share an identity and onDuplicate() is Fail
MaxErrorsExceededExceptionThe failed-row threshold was reached and the run aborted
ApiFetchExceptionAn API sync's fetch failed after its retries, or its paging looped
DatabaseFetchExceptionA database sync's fetch failed, or its source does not match it

The first two are configuration problems: catching them at run time is usually the wrong move, because sync:doctor would have caught them at deploy time. The others are per-file or per-fetch conditions worth handling in a listener or a wrapper command.

DataSyncException does not cover I/O. Failures that are neither configuration nor data, an unreadable staged file, a destination template that will not render, a checksum that cannot be computed, throw a plain RuntimeException, and an unreachable disk surfaces as whatever Flysystem or Laravel threw. Since DataSyncException extends RuntimeException, catching RuntimeException covers both; catch DataSyncException when you specifically want the package's own conditions.

Once a run exists, anything thrown while processing it is recorded on the run. The status becomes failed, the message enters error_summary, and the staged file is preserved before the exception is rethrown. A failure that reached a file is readable in sync:status whether or not anything caught it. Configuration and definition errors throw before a run is recorded, which is why they only ever show up in the command output and in sync:doctor.

Nothing was processed

"No enabled sync definitions are registered." data-sync.syncs is empty, or every definition is disabled through overrides.<name>.enabled => false. Name one explicitly to run it anyway.

"Skipped [products] because discovery is already running." Another sync:run holds the definition's five-minute discovery lock. This exits zero on purpose. If it persists with nothing running, the lock's cache store is misbehaving, sync:doctor verifies it.

The summary reports 0 discovered, 1 skipped. The checksum has already completed. That is the ledger working. Change the file, or use --force. See file identity.

The summary reports files as deferred. minFileAge() is holding them back until they are old enough. Expected while a supplier is still uploading; unexpected if the source's clock is far ahead of yours.

The summary reports 0 discovered, 0 skipped, 0 deferred. The source directory is empty from the package's point of view. Check the disk root, the path(), and, for a Source::connection(), whether root in the connection config already includes part of the path.

Definition and configuration errors

These all throw at boot or on the first resolve, so they surface in sync:doctor too.

"[…] is missing required source columns […]. Actual header: […]" A field that is neither optional() nor default() names a column the file does not have. The message prints the header actually found, usually a renamed column, a stray BOM, or a wrong delimiter making one giant column.

"[…] contains unknown source columns […]" onUnknownColumns() is Strict and the file grew a column. Map it, mark the policy Ignore, or capture it.

"[…] fields() maps […], which is not a column or writable attribute on the model returned by model()." A typo, or a migration that has not run. The check is skipped when the table does not exist, so this appears once the table is present.

"[…] matchOn() references […], but fields() does not map it." Match columns must be mapped fields. Add a Field for it, even if the value is only used for matching.

"[…] captureUnmappedInto() targets […], which must have an array-compatible Eloquent cast." Cast the attribute to array, json, object, collection, encrypted:array, or encrypted:collection.

"No sync definition named […] is registered in [data-sync.syncs]." The name is derived from the class, ProductsSync becomes products. Check name() and the syncs list.

"The [data-sync.overrides.…] config does not match a registered sync definition." An override keyed on a name that no longer exists, usually after renaming a definition.

"The [data-sync.overrides.….…] config is not allowed." Overrides accept only enabled, schedule, queue, and chunk_size. Everything else belongs on the definition class.

"The [data-sync.connections.…] connection uses [sftp], which requires composer require league/flysystem-sftp-v3." Install the adapter. FTP needs league/flysystem-ftp.

"The [data-sync.store_failed_payloads] config was renamed to [data-sync.capture_failed_payloads] in 1.1." Rename the key in config/data-sync.php. See upgrading from 1.0.

"[…] endpoint() returned […], which is not an http or https URL." Return an absolute URL from endpoint(), including the scheme.

"[…] timeout() must return a number of seconds greater than zero." Return 1 or more from timeout(). retries() and retryDelay() have the same kind of check and accept 0. lockTimeout() and pageSize() must also return 1 or more.

"[…] connection() returned […], which is not configured in [database.connections]." Add the connection to config/database.php. A definition provider must register the connection before it returns the definition. See register the connection.

"[…] query() sets a limit or offset." Remove limit(), take(), offset(), and skip() from query(). The package pages the query by keyColumns() and pageSize().

"[…] watermark() uses Watermark::rowVersion(), which needs a sqlsrv connection…" A rowversion watermark works on SQL Server only. Use Watermark::timestamp() on other drivers.

"[…] name() returned […]. A name a definition provider returns may use only letters, digits, hyphens, and underscores…" Build provider names from IDs, such as orders-{$accountId}-{$sourceId}, not from labels that an account types. A dot is not allowed, because it splits a data-sync.overrides key.

"[…] definitions() returned a […], but every item must extend…" A provider returned something other than a SyncDefinition or a TransferDefinition, such as a model or null.

Rows are missing or wrong

Rows are counted as skipped. Three causes, in rough order of likelihood: the write mode declined them (CreateOnly on existing rows, UpdateOnly on new ones); they lost duplicate resolution inside the file; or a beforeSave() hook returned false.

"[…] found duplicate identity […] on rows 12 and 84; onDuplicate() is set to fail." Two rows share the same matchOn() values. Switch to LastWins or FirstWins if the feed legitimately repeats keys.

Values arrive as strings. Readers produce strings and nulls by design. Convert with transform() or with model casts.

"Column […] holds invalid JSON for the JSON-cast […] attribute: …" The field maps to an attribute with a JSON cast, so the package decodes the value before it fills the model. The source sent text that is not JSON. The row fails and the run continues. Fix the source value, or map the column to an attribute without a JSON cast. See JSON attributes.

A JSON column holds a quoted string such as "{\"a\":1}". Version 2.0 wrote the row, and 2.0 encoded a JSON string a second time. From 2.1 the package decodes the string first. To rewrite the row, run sync:retry on the run that wrote it. The retry processes the archived snapshot again and does not move the watermark.

Dates from an XLSX file look like 2026-07-31T00:00:00+00:00. Cell values are normalized to ISO-8601 strings before mapping. A datetime cast on the model handles them directly.

Only part of an XLSX workbook was imported. Only the first sheet is read. Nothing else is looked at.

Model observers did not fire. The definition uses the bulk write strategy. Declare beforeSave() or afterSave() to switch to Eloquent writes, or move the work into a SyncCompleted listener. sync:status prints which strategy each run used.

A default was not applied. default() applies when the row has no entry for the column at all. A blank CSV cell is an empty string, and a short delimited row yields null, both are values.

"[…] stopped after exceeding maxErrors() […]" The failed-row threshold was reached and the run aborted; on the queued path the batch is cancelled too. Fix the feed and retry the run.

Runs that fail or hang

A run sits at running with no batch progress. Usually a dead worker. Restart it; the batch resumes. If the batch is gone, retry the run. sync:doctor warns about runs running for more than an hour.

"Unable to read staged file […]" The worker cannot see the staged copy. On a multi-server deployment this is almost always a local staging disk, point staging, archive, and failed at shared storage. See queues and workers.

"Archive checksum verification failed for run […]" The copy written to the archive disk does not match the source bytes. The copy is deleted and the run fails, and, importantly, the source file is left alone. Check the archive disk for truncation, quota, or a proxy rewriting content.

The run completed but the supplier's file is still there. Source cleanup is opt-in: copyTo(), moveTo(), or deleteOriginal(). If one is configured and the file remains, look at sync_runs.warnings, a cleanup failure is recorded there rather than failing an otherwise successful import.

The batch was cancelled. Either maxErrors() tripped it or something cancelled it externally. The run is cancelled, the staged file is preserved, and a retry restores from the failed disk.

API syncs

"[…] could not fetch […]: …" The fetch failed after its retries. The text after the colon is the HTTP client's error. A 401 or 403 points at the credentials in request(). A 5xx or a connection error points at the remote system. The package records a failed run and keeps the watermark, so the next scheduled fetch asks for the same window. See when a fetch fails.

"[…] was sent back to […], which it had already fetched." nextPage() returned a URL from earlier in the same fetch. This usually means the endpoint still returns a link on its last page, such as the current page's own URL. Return null when the response says there are no more pages.

The watermark does not move. It moves only when a run finishes as completed. Check sync:status for completed_with_errors or failed runs. Also check that watermarkFrom() returns a value on at least one page. A null from every page keeps the previous value.

A fetch is listed as skipped. The endpoint returned the same bytes as a snapshot that already completed. That is the ledger working. Use --force to process it again.

sync:run with no name stopped before some definitions ran. The command stops at the first definition that throws, and a failed fetch throws. Scheduled runs call sync:run <name> for each definition, so one failing endpoint does not hold up the others there.

Database syncs

"[…] could not read from the […] connection: …" The query failed on the source. The text after the colon is the driver's error. The package records a failed run and keeps the watermark, so the next scheduled fetch reads the same window. Run sync:doctor to check the connection and the PDO extension.

"[…] does not match its source. …" A mapped field, a key column, or the watermark column is missing from the source table, or the watermark column has the wrong type. The message names each column to fix. The fetch reads no rows until the table matches.

"[…] read a binary value from column […], which a snapshot cannot store." The query selects a binary column, such as varbinary or rowversion. Drop it from query(), or on SQL Server select it as text with CONVERT(varchar(max), column, 1).

"[…] keyColumns() names […], but the rows query() returns have no such column." Add the key column to the select() in query(). A qualified key such as orders.id must come back as id.

"[…] read a row whose key column […] is null." Key columns must never be null. Pick columns with a NOT NULL constraint, or filter out null keys in query().

"[…] watermark() now reads […] column […], but the stored watermark holds…" You changed the watermark mode or column. Delete the sync_watermarks row that the message names. The next run then reads every row again, and the upsert on matchOn() updates the rows you already have.

The same rows are read on every run, and the window grows. One or more rows fail on every run, so no run finishes as completed and the watermark does not move. sync:status warns about this after data-sync.stalled_watermark_runs runs. List the failures with sync:status <name> --failures, fix them, and replay them with sync:retry.

A row deleted in the source is still in your table. The package never deletes models for rows that stop appearing, and a watermark only sees rows that still exist. If the source marks deleted rows with a column, map that column and filter on it in your application.

Retries

"Run […] has not finished and cannot be retried." Only terminal runs can be retried. Wait for it, or cancel its batch.

"Run […] has no archived or failed-file copy to retry." The run never got far enough to preserve a copy, or its archive_path is empty. Re-deliver the file instead.

"No run or archived file matching […] was found." The run was pruned and no stored file matches the reference. See retention and pruning.

"The archived file […] for this run no longer exists." The run outlived its file. runs_days is longer than archive_days by design. Increase archive_days if retries this old need to work.

--failed-rows refuses to run. Either payload capture was off for the original run, see sensitive data, or the run is a transfer, which has no rows. Retry the whole file.

Transfers

"Filename […] does not match […]" Recorded as a skipped run under the default UnmatchedFilenamePolicy::Skip, and the checksum is marked processed so the file is not seen again. Use Fail if a stray filename should be an error.

"destination()->path() references unknown value […]" The path template names a placeholder that is neither a captured segment nor one of the built-in values. See destination paths.

"filenamePattern() pattern […] must contain at least one {segment}." A pattern needs at least one placeholder; a fully literal filename is not a pattern.

"Destination file […] already exists with a different checksum." ExistingFilePolicy::Fail is in force and something else claims that path. Either the path template is not unique enough, or the supplier re-issued a document under the same name.

Disks and growth

The staging disk keeps growing. Successful runs leave their staged copy behind and sync:prune does not sweep staging. Add your own cleanup, see retention and pruning.

sync:doctor --strict always fails on one machine. The local-staging-disk warning is expected on a single-server deployment. Use plain sync:doctor there, or move staging to shared storage.

Memory spikes on a JSON feed. Format::json() loads the whole document. Use NDJSON for anything large. See readers.

Still stuck

Collect the run ULID, the output of sync:status <name> --failures, and the output of sync:doctor. Between them they identify the definition, the file, the write strategy, the counters, and the environment, which is most of what any diagnosis needs.

What to read next

  • Run history and status to collect the run details used during diagnosis.
  • Health checks to verify configuration, storage, and the database before running a feed.
  • Failures and retries to recover a failed file or selected rows.
PreviousTesting
View source

On this page

  1. Catching package exceptions
  2. Nothing was processed
  3. Definition and configuration errors
  4. Rows are missing or wrong
  5. Runs that fail or hang
  6. API syncs
  7. Database syncs
  8. Retries
  9. Transfers
  10. Disks and growth
  11. Still stuck
  12. What to read next