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 Bigquery Laravel Package

pelfox/laravel-bigquery

Use Laravel's Query Builder and Eloquent with Google BigQuery. Configure a BigQuery connection or use the facade, query tables/datasets, and map BigQuery types with dedicated Eloquent casts, including repeated fields and structs.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up (≈30 minutes to first query):

composer require pelfox/laravel-bigquery

Add a secondary connection in config/database.php — this package registers a database driver, so it slots into Laravel's existing connection manager rather than introducing a new SDK:

'bigquery' => [
    'driver'     => 'bigquery',
    'database'   => '',
    'prefix'     => '',
    'dataset'    => env('BQ_DATASET'),          // required
    'keyFilePath' => env('BQ_KEY_FILE_PATH'),   // required
],

Then confirm reachability before writing any application code:

DB::connection('bigquery')->table('your_table')->limit(5)->get();

If that returns rows, the hard part is done. Everything after this is a design decision, not an integration problem.

Where to look first, in this order:

  1. The Pelfox\LaravelBigQuery\Eloquent\Casts namespace — this is the actual value of the package. Without it you are hand-coercing NUMERIC, BIGNUMERIC, BYTES, TIMESTAMP, STRUCT, JSON, and repeated fields, and that is exactly where precision and timezone bugs breed. Read the cast table in the README before you write a single model.
  2. The README's example model — note the two lines that tell you what you're working with: public $incrementing = false; and public $timestamps = false;. Internalise that before you reach for save().
  3. The release notes (one line each) — brief, but they tell you what has historically been broken: 1.5.1 fixes escaping of strings with line breaks and repeated-cast coercion; 1.5.2 adds informative SQL errors and Escape::json(). That history is a signal about where to focus your own testing.

First use case to build: a read-only internal report — a filtered, aggregated, LIMIT-bounded query behind an admin screen or an artisan command. Do not start with a write path, a model with relationships, or anything on a user-facing request.

Implementation Patterns

Pick the Query Builder as your default, Eloquent as the exception. BigQuery does not honour transactions, auto-incrementing keys, timestamps, model events, or read-after-write consistency. Every one of those is an Eloquent default. The Query Builder imports none of those assumptions and makes it visually obvious at the call site that this is an analytical read:

$rows = DB::connection('bigquery')
    ->table('events')
    ->selectRaw('DATE(occurred_at) as day, COUNT(*) as total')
    ->where('occurred_at', '>=', $since)
    ->groupBy('day')
    ->get();

Reach for an Eloquent model only when you genuinely want a read model with casts and accessors attached.

Isolate all package-specific code behind a boundary. This is the single highest-value architectural decision here. Put every query, cast definition, and struct schema in one place — app/Queries/BigQuery/ or a repository layer — so that "replace this package" is a swap, not a refactor:

app/Queries/BigQuery/
├── DailyEventCountsQuery.php
├── RevenueByCohortQuery.php
└── Concerns/           # shared casts, struct schemas

Given four stars, zero dependents, and a single maintainer, assume you will eventually own a fork. A clean seam is what makes that cheap.

Dataset routing: prefer the connection default, override deliberately. The connection's dataset key handles the common case. Dataset-qualified names are available for cross-dataset reads:

DB::connection('bigquery')->table('other_dataset.table')->get();

// Eloquent
protected $table = 'other_dataset.table';

Use qualification sparingly. It makes environment routing implicit — a typo silently queries the wrong dataset, and a hard-coded dataset name in a $table property is an environment leak waiting to happen. Where you must qualify, build the name from config, not a literal.

Map every column through a cast, including repeated and struct fields. Don't rely on default coercion:

protected $casts = [
    'amount'   => AsNumeric::class,                  // never a plain float
    'ids'      => AsInteger::class . ':1',           // ':1' = repeated mode
    'payload'  => AsJson::class,
    'meta'     => AsStruct::class . ':0,metaSchema', // ':0' + schema method
];

public function metaSchema(): array
{
    return [
        'source' => StringType::class,
        'count'  => IntegerType::class,
        'tags'   => [StringType::class],             // repeated within struct
    ];
}

The struct schema must mirror the BigQuery schema exactly — nested arrays, repeated markers, and all — or inserts will error. Treat the schema method as a contract and pin it with a test that inserts and reads back.

Verify casts by round-tripping, not by reading the README. For NUMERIC, BIGNUMERIC, TIMESTAMP with non-UTC data, BYTES, JSON, repeated scalars, and nested structs: write a row, read it back, assert equality. Silent precision loss and timezone drift are the highest-severity failure modes this package has, because they don't throw — they surface in a report three weeks later.

Use Escape::json() when hand-building JSON fragments. It exists specifically because JSON literal escaping is easy to get subtly wrong:

use Pelfox\LaravelBigQuery\Escape;

->whereRaw('json_field = ' . Escape::json($value))

Prefer bindings wherever the builder supports them; treat whereRaw as a last resort that requires review.

Keep credentials out of the repo and out of logs. keyFilePath is a long-lived static secret on disk. Source it from your secrets manager at deploy time via env(), never commit the JSON, and make sure it isn't echoed in CI. Write a rotation runbook — this is a deliberate decision, not a config line copied from a README.

Gotchas and Tips

SELECT * is a billing event, not a convenience. BigQuery charges by bytes scanned, and this package gives you no dry-run, no cost estimate, and no default LIMIT. An ORM that makes unbounded queries feel free is a cost hazard. Enforce guardrails at the project level — quota limits, maximum_bytes_billed, partition filters — outside this package, because you cannot enforce them inside it. Always filter on partitioned/clustered columns, and put a LIMIT on anything exploratory.

Escaping has a bug history — review every user-supplied value in a where. Releases 1.5.1 and 1.5.2 both fixed value-escaping defects (line-broken strings, repeated-cast coercion to string). That is a security-relevant signal for a query builder. Prefer bound parameters, and get an independent review on any path where request data reaches a where clause.

Never assume save() round-trips. Set public $incrementing = false; and public $timestamps = false; on every model, and don't rely on model events firing or on a write being immediately visible. update/delete without a where used to error before 1.0.4 — the fix means an unscoped mass update is now possible. Keep an eye on it.

Model events and relationships are traps. HasUuids works (it's in the README example) because it generates the key client-side. belongsTo/hasMany assume local joins with predictable latency; BigQuery joins are expensive and slow. Do the join in a single query with the Query Builder rather than traversing relationships.

Don't trust the version number alone. v3.0.0 shipped with no changelog notes beyond a compare link, and releases track Laravel majors on the maintainer's schedule rather than yours. Before upgrading, read the diff. Pin the version in composer.json so a framework upgrade doesn't silently drag the BigQuery driver with it.

Pagination isn't tuned for BigQuery. paginate() will produce correct-but-expensive OFFSET-style paging over large result sets. For anything sizeable, cache aggregates into a local table or push results to a materialised summary and page over that.

Debugging: lean on the improved SQL errors from 1.5.2. When a query fails, the package now surfaces a more informative error than the raw client. If you get a schema-mismatch error on insert, the cause is almost always a struct schema method that doesn't match the actual BigQuery schema — diff them field by field, including repetition.

Two things to decide before this reaches production:

  1. Blast radius. If a query scans 10 TB, who is paged? Answer this with project-level quotas, not with code review discipline.
  2. The fork trigger. Write down the condition — maintainer goes quiet for N months, or a Laravel major lands that blocks your upgrade — at which you fork or replace with a thin wrapper over google/cloud-bigquery. Because you kept everything behind app/Queries/BigQuery/, that switch is a swap, not a rewrite.

And ask whether PHP is the right place at all. For recurring reports, a scheduled job writing to a cache table beats a web request hitting BigQuery. This package is excellent for interactive, low-frequency internal reads, and a poor fit for anything on a user-facing request path or with a sub-second latency expectation.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin