Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Sql Loader Laravel Package

yajra/laravel-sql-loader

Yajra's Laravel SQL Loader provides a simple way to import and execute .sql files in Laravel. Use Artisan and your configured database connections to run schema dumps, seed scripts, or bulk SQL, making setup, deployments, and data loading more repeatable across environments without manual copy-paste...

View on GitHub
Deep Wiki
Context7

Getting Started

First, resolve what you're installing. The package's public metadata is inconsistent: the repo description and PM notes describe Oracle SQL*Loader (sqlldr, control files, CHARACTERSET in v1.10.0), while description.md claims generic SQL-dump import. The release notes corroborate Oracle. Confirm before writing any code:

composer show yajra/laravel-sql-loader          # inspect requires
grep -ri "sqlldr\|oci8\|control" vendor/yajra/laravel-sql-loader/src | head

If it shells out to sqlldr, everything below applies. If you need MySQL/PostgreSQL/SQLite bulk import, stop — this is not that package.

Prerequisites (the real cost, not the composer require):

  • Oracle Instant Client (or full client) on every host that runs the loader — app servers, queue workers, scheduler, CI.
  • sqlldr reachable on PATH, plus ORACLE_HOME / LD_LIBRARY_PATH where the client needs them.
  • TNS entry or Easy Connect string, a configured Oracle connection in config/database.php.
  • A staging table and an Oracle user with INSERT on it plus write access to the directory sqlldr uses.
  • A DBA ticket. This is the long pole; raise it before the engineering spike.

Install and configure:

composer require yajra/laravel-sql-loader:^1.10   # ^1.10 minimum if any non-ASCII data is in scope
php artisan vendor:publish --provider="Yajra\SqlLoader\SqlLoaderServiceProvider"

Pin the binary path, default connection, and a per-environment scratch directory in the published config, e.g. storage/app/sqlldr/. Never hardcode it — dev on macOS, CI in a container, and prod on a RHEL host will all differ.

The first use case: load a CSV into a staging table.

  1. Create the staging table in Oracle (all columns VARCHAR2/nullable is the pragmatic default for raw feeds).
  2. Write a control file as a versioned artefact, database/loaders/orders.ctl:
OPTIONS (SKIP=1)
LOAD DATA
CHARACTERSET UTF8
INFILE 'orders.csv'
APPEND
INTO TABLE orders_staging
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
TRAILING NULLCOLS
(
  order_id,
  customer_id,
  order_date DATE "YYYY-MM-DD",
  amount
)
  1. Drive it from code. The call looks roughly like the fluent shape below — verify exact method names against the package README before relying on them:
use Yajra\SqlLoader\Facades\SQLLoader;

$run = storage_path('app/sqlldr/orders/'.now()->format('YmdHis'));
@mkdir($run, 0775, true);

SQLLoader::connection('oracle')
    ->controlFile(database_path('loaders/orders.ctl'))
    ->dataFile($csvPath)
    ->logFile("{$run}/orders.log")
    ->badFile("{$run}/orders.bad")
    ->discardFile("{$run}/orders.dsc")
    ->load();
  1. Check the log before you check the exit code. A successful-looking run can reject thousands of rows into .bad. See Gotchas.

Definition of "first use case done": one feed loaded into staging, row count in == row count committed, .bad empty, and the run reproduces from a clean checkout plus a documented control file.

Implementation Patterns

Control files as code. Keep one .ctl per target table under database/loaders/, committed, reviewed in the same PR as the migration that changes its table. This is not optional discipline — it's the only thing preventing silent field-mapping drift. Add a one-line rule to the team's review checklist: migration touching a loaded table ⇒ control file updated.

Wrap it in exactly one service class. The package surface is small and the upstream is effectively single-maintainer (9 stars, 0 dependents). Isolate it:

final class OracleFeedLoader
{
    public function __construct(private string $connection) {}

    public function load(FeedDefinition $feed, string $csvPath): LoadResult
    {
        // validate file, build run dir, invoke SQLLoader, parse log, return DTO
    }
}

Everything else in the codebase depends on LoadResult, not the facade. When you need to fork or swap the package, you change one file.

Load-to-staging, validate, then swap. Never load directly into a live table — SQL*Loader gives you no transactional rollback, so a mid-load failure leaves a half-populated table. The pattern:

  1. TRUNCATE the staging table (or use REPLACE in the control file).
  2. Load.
  3. Assert rows_read == rows_committed and that .bad/.discard are empty.
  4. Promote: INSERT INTO target SELECT ... FROM staging for modest volumes, or partition exchange for large ones.
  5. Record the run id, counts, and file checksum.

Return a result object, not a boolean. Expose rows read, rows committed, rows rejected, log path, run directory. Every caller should be able to alert on divergence without knowing what sqlldr is.

Queue it and serialise it per table. Loads scale with file size and badly with concurrency — parallel loads into the same table contend on locks. Use WithoutOverlapping keyed on the target table:

public function middleware(): array
{
    return [(new WithoutOverlapping("oracle-load:{$this->feed->table}"))->expireAfter(3600)];
}

Schedule outside peak traffic; a large direct-path load holds I/O and locks for a long time.

Per-run directories, never reuse paths. storage/app/sqlldr/{feed}/{runId}/ containing *.ctl, *.log, *.bad, *.dsc. You get an audit trail per run, you can't accidentally overwrite the log you need for debugging, and pruning is a single find -mtime +30 -delete. Rotate and prune on a schedule — these logs grow fast.

An Artisan command for ops, a job for the app. The command is for humans and deploys (feed:load orders /path/file.csv --truncate); the queued job is for scheduled and admin-triggered imports. Both call the same service.

Character set handling (v1.10.0+). Declare it in two places, deliberately:

  • CHARACTERSET UTF8 (or the DBA-mandated value, often AL32UTF8) in the control file.
  • NLS_LANG in the environment of the actual process running sqlldr — e.g. AMERICAN_AMERICA.AL32UTF8. The control-file clause does not fix a mis-set client session; both must agree.

Test the seam, not sqlldr. Behind an interface, unit tests assert the generated control file (snapshot) and the log-parsing logic with fixture logs — including a log with rejects. Reserve a real database for an opt-in integration test in CI with a service container and Instant Client installed.

Admin-triggered uploads. Upload to a quarantine disk, validate extension/size/content, generate the run directory and control file server-side, then dispatch the job. Never let user input reach a path or a shell argument.

Gotchas and Tips

Silent bad rows are the top hazard. sqlldr writes rejected records to .bad and discarded records to .discard and can report success while doing so. Every load must: parse the log for Rows successfully loaded and Rows not loaded due to data errors, confirm both bad files are absent or empty, and compare rows-in to rows-committed. Emit those counts as metrics and alert on any divergence. Treating the exit code as the verdict is how you ship 40% of a feed and don't notice for a week.

Environment differences, not code differences, cause most failures. sqlldr runs as the calling process's OS user. php artisan tinker on your laptop and a queue worker under php-fpm are different users with different PATH, ORACLE_HOME, NLS_LANG, open_basedir, and exec permissions. Reproduce any failure in the real execution context — dispatch the actual job, don't hand-run the command — before concluding it's a code bug.

Set Oracle environment variables explicitly in config, not via the shell profile. FPM and queue workers usually don't inherit your .bashrc. A load that works from SSH and fails from the scheduler is almost always this.

Path and credential interpolation. The wrapper builds a command line from your paths and connection details. Keep filenames strictly [A-Za-z0-9._-], keep them under storage/, and prefer Oracle Wallet / external authentication over an inline password on the command line (ps shows command lines to every user on the host).

SKIP=1 is a footgun. It's only for files with a header row. Get it wrong in either direction and you either drop a real record or try to load your column names as data — and the latter often fails quietly into .bad.

Date masks must match the file. DATE "YYYY-MM-DD" in the control file versus a DD/MM/YYYY feed produces rejects, not exceptions. Cross-check the masks against a real sample whenever a partner changes their export format.

CRLF and trailing delimiters. Windows-generated CSVs leave a \r on the last field of every line. It loads as data. Preprocess or account for it; verify with a SELECT DUMP(last_col) after the first load of any new feed.

Zero-byte files load "successfully" with zero rows. Guard on file size and minimum expected row count before dispatching.

APPEND vs REPLACE vs TRUNCATE changes what you destroy. REPLACE empties the table before loading. If someone "tidies up" the control file, that's a data-loss change disguised as a formatting diff — which is another reason control files belong in code review.

Charset bugs are invisible until they aren't. Visual inspection of an accented name in a terminal proves nothing. Round-trip a known sample and compare bytes (SELECT DUMP(col, 1016)) or a checksum against the source. Do this once per new feed and after any Oracle client upgrade.

Control file / schema drift is silent. A migration adding a column doesn't break the load — it just means that column is now NULL for every loaded row, forever. If you do one thing from Tips: add a CI check comparing the control file's field list to ALL_TAB_COLUMNS.

Keep a documented fallback. 9 stars, 0 dependents, effectively one maintainer, and a package that must be re-verified against every Laravel major. Given how thin the surface is, a vendored patch or a 200-line replacement is realistic — quantify that exit cost once, write it down, and revisit only when a Laravel upgrade or an sqlldr version change forces the question.

Instrument the failure modes you can't see from PHP: rejected rows, partial loads, encoding regressions after a client upgrade, privilege loss after a DBA change, control-file drift, and disk exhaustion in the log directory.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package