nomansheikh / laravel-bigquery-eloquent
Query Google BigQuery tables with Laravel Eloquent.
Package info
github.com/nomansheikh/laravel-bigquery-eloquent
pkg:composer/nomansheikh/laravel-bigquery-eloquent
Fund package maintenance!
Requires
- php: ^8.3
- google/cloud-bigquery: ^1.34
- illuminate/contracts: ^11.0||^12.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^2.9||^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1||^7.10.0
- orchestra/testbench: ^10.0.0||^9.0.0||^8.22.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-arch: ^3.0
- pestphp/pest-plugin-laravel: ^3.2
- phpstan/extension-installer: ^1.3
- phpstan/phpstan-deprecation-rules: ^1.1||^2.0
- phpstan/phpstan-phpunit: ^1.3||^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-14 09:35:49 UTC
README
Overview
Laravel BigQuery Eloquent is a Laravel package that seamlessly integrates Google BigQuery with Laravel's Eloquent ORM. It enables you to query BigQuery tables using familiar Eloquent syntax, simplifying analytics and data querying directly within your Laravel applications.
Features
- Eloquent Integration: Use BigQuery tables as Eloquent models, including relations,
find(), aggregates, and pagination. - Dedicated BigQuery Driver: Optimized database driver for BigQuery.
- Automatic Fully Qualified Table Names: Handles
project.dataset.tableformatting transparently. - Custom Query Grammar: Generates GoogleSQL —
EXTRACTfor date parts,RAND(),JSON_VALUE(), and correctly backtick-quoted identifiers. - Full DML Support:
select,insert,update, anddeletevia Eloquent or the raw query builder, executed as BigQuery DML. - Raw Statements:
DB::statement(),DB::select(),DB::update(),DB::cursor(), andDB::pretend()all work against BigQuery. - Bindings That Just Work:
Carbon/DateTimeInterfacevalues are auto-wrapped as BigQueryTimestamp, microseconds are preserved, andnullbindings are handled. - Native Values Unwrapped:
TIMESTAMP,DATE,NUMERIC,BYTES, and nestedSTRUCT/ARRAYvalues arrive as plain PHP values that Eloquent casts understand. - Cost Controls: Per-connection
maximum_bytes_billed,job_timeout_ms, and joblabels. - Flexible Authentication: Supports Application Default Credentials (ADC) and service account key files.
- Environment Configuration: Easy setup via environment variables.
Requirements
- PHP 8.3 or higher
- Laravel 12.x or 13.x
- Access to Google Cloud BigQuery API
- Google Cloud authentication (Application Default Credentials recommended)
Installation
Install the package via Composer:
composer require nomansheikh/laravel-bigquery-eloquent
Configuration
1. Publish the configuration file
Run the following Artisan command to publish the package config:
php artisan vendor:publish --tag="bigquery-eloquent-config"
2. Authentication Setup
The package supports Google Cloud's recommended authentication hierarchy:
-
Recommended: Application Default Credentials (ADC)
- Local development: Run
gcloud auth application-default login - Production: Use a service account attached to your compute instance or set the
GOOGLE_APPLICATION_CREDENTIALSenvironment variable.
- Local development: Run
-
Alternative: Service Account Key File
- Download a JSON key file from Google Cloud Console.
- Set the
BIGQUERY_KEY_FILEenvironment variable pointing to the JSON file (not recommended for production).
How credentials are resolved
key_file accepts either a path to a JSON key file or the credentials array itself.
If it is set, it is used directly. If it is empty or absent, the Google client falls
back to Application Default Credentials, which it resolves in this order:
- The
GOOGLE_APPLICATION_CREDENTIALSenvironment variable. - The well-known ADC file (
gcloud auth application-default loginwrites this). - The App Engine built-in service account.
- The Compute Engine / GKE / Cloud Run metadata service.
Example credentials array in config/database.php:
'bigquery' => [ 'driver' => 'bigquery', 'project_id' => env('BIGQUERY_PROJECT_ID', ''), 'dataset' => env('BIGQUERY_DATASET', ''), 'key_file' => [ 'type' => env('GOOGLE_CLOUD_ACCOUNT_TYPE'), 'private_key_id' => env('GOOGLE_CLOUD_PRIVATE_KEY_ID'), 'private_key' => env('GOOGLE_CLOUD_PRIVATE_KEY'), 'client_email' => env('GOOGLE_CLOUD_CLIENT_EMAIL'), 'client_id' => env('GOOGLE_CLOUD_CLIENT_ID'), 'auth_uri' => env('GOOGLE_CLOUD_AUTH_URI'), 'token_uri' => env('GOOGLE_CLOUD_TOKEN_URI'), 'auth_provider_x509_cert_url' => env('GOOGLE_CLOUD_AUTH_PROVIDER_CERT_URL'), 'client_x509_cert_url' => env('GOOGLE_CLOUD_CLIENT_CERT_URL'), ], ],
3. Environment Variables
Add the following to your .env file:
BIGQUERY_PROJECT_ID=your-project-id BIGQUERY_DATASET=your-dataset-name # Required for datasets outside the US multi-region, e.g. "EU" or "asia-northeast1" # BIGQUERY_LOCATION=EU # Optional cost and runtime guards # BIGQUERY_MAXIMUM_BYTES_BILLED=10737418240 # BIGQUERY_JOB_TIMEOUT_MS=60000 # Optional: Only if using service account key file (not recommended for production) # BIGQUERY_KEY_FILE=path/to/your/service-account-key.json
4. Database Connection
Add the BigQuery connection in config/database.php:
'connections' => [ // ... other connections ... 'bigquery' => [ 'driver' => 'bigquery', 'project_id' => env('BIGQUERY_PROJECT_ID', ''), 'dataset' => env('BIGQUERY_DATASET', ''), // The region the dataset lives in. Queries against a dataset outside the // US multi-region fail unless this is set. 'location' => env('BIGQUERY_LOCATION'), // Cancels a query before it is billed if the planner estimates it will // scan more than this many bytes. 'maximum_bytes_billed' => env('BIGQUERY_MAXIMUM_BYTES_BILLED'), 'job_timeout_ms' => env('BIGQUERY_JOB_TIMEOUT_MS'), // Attached to every job, useful for attributing BigQuery spend. 'labels' => ['service' => 'my-app'], // Optional: Only if using service account key file (not recommended) 'key_file' => env('BIGQUERY_KEY_FILE', ''), ], ],
Every key also has a package-level default in config/bigquery-eloquent.php, which is used
when the connection itself does not define it.
Usage
Models
Extend BigQueryModel to interact with BigQuery tables. Because BigQuery has no auto-incrementing
primary keys, BigQueryModel defaults to $incrementing = false and $keyType = 'string', so
models assign their own keys (typically a ULID or UUID):
<?php namespace App\Models; use Illuminate\Database\Eloquent\Concerns\HasUlids; use NomanSheikh\LaravelBigqueryEloquent\Eloquent\BigQueryModel; class UserAnalytics extends BigQueryModel { use HasUlids; protected $table = 'user_analytics'; // Automatically prefixed with project.dataset protected $fillable = ['user_id', 'page_views', 'session_duration']; }
Timestamps. Eloquent's timestamps are on by default, so the BigQuery table needs
created_atandupdated_atTIMESTAMPcolumns. Setpublic $timestamps = false;on the model if it does not have them, otherwise every write fails.
Reading
// Basic query $users = UserAnalytics::where('page_views', '>', 100)->get(); // Complex query with ordering and limits $topUsers = UserAnalytics::select('user_id', 'page_views') ->where('created_at', '>=', now()->subDays(30)) ->orderBy('page_views', 'desc') ->limit(10) ->get(); // Aggregations $stats = UserAnalytics::selectRaw(' COUNT(*) as total_users, AVG(page_views) as avg_page_views, SUM(session_duration) as total_duration ')->first();
Writing
insert, update, and delete are executed as BigQuery DML statements. Be aware of BigQuery's DML quotas — DML is intended for batch and analytical workloads, not high-frequency OLTP writes.
// Insert via Eloquent $row = UserAnalytics::create([ 'user_id' => 'usr_42', 'page_views' => 17, 'session_duration' => 312, ]); // Update UserAnalytics::where('user_id', 'usr_42')->update(['page_views' => 18]); // Delete UserAnalytics::where('user_id', 'usr_42')->delete(); // Batch insert via the query builder DB::connection('bigquery')->table('project.dataset.user_analytics')->insert([ ['user_id' => 'usr_1', 'page_views' => 5], ['user_id' => 'usr_2', 'page_views' => 9], ]);
Binding types
Carbon / DateTimeInterface values are wrapped as a BigQuery Timestamp, preserving
microseconds. That is the right type for a TIMESTAMP column, but BigQuery will not
compare a TIMESTAMP parameter against a DATE, DATETIME, or TIME column, and it
will not compare a STRING parameter against a NUMERIC column:
No matching signature for operator = for argument types: DATE, TIMESTAMP
The driver cannot infer the column type from a PHP value, so pass the matching BigQuery value object for those columns. They are forwarded to the API untouched:
$client = DB::connection('bigquery')->getClient(); UserAnalytics::where('signup_date', $client->date(now()))->get(); // DATE UserAnalytics::where('clock_in', $client->time(now()))->get(); // TIME UserAnalytics::where('revenue', $client->numeric('1250.75'))->get(); // NUMERIC UserAnalytics::where('blob', $client->bytes($binary))->get(); // BYTES
whereDate(), whereMonth(), whereYear() and whereTime() already cast their
parameters, so they work on TIMESTAMP and DATE columns without this.
Reading values
BigQuery's SDK value objects are unwrapped before they reach your model, so $casts
behaves normally:
| BigQuery type | PHP value |
|---|---|
TIMESTAMP / DATETIME |
string, microsecond precision, castable to datetime |
DATE / TIME |
Y-m-d / H:i:s.u string |
NUMERIC / BIGNUMERIC |
string, so precision a float would lose is kept |
BYTES |
raw binary string |
JSON / GEOGRAPHY |
string |
STRUCT / ARRAY |
array, unwrapped recursively |
Raw Queries
Execute raw SQL directly via the BigQuery connection:
use Illuminate\Support\Facades\DB; $results = DB::connection('bigquery')->select( 'SELECT user_id, COUNT(*) as visits FROM `project.dataset.user_analytics` WHERE created_at >= ?', [now()->subDays(7)] ); // DDL and any other statement BigQuery accepts DB::connection('bigquery')->statement( 'CREATE TABLE IF NOT EXISTS `project.dataset.user_analytics` (user_id STRING, page_views INT64)' ); // Stream a large result set without buffering it foreach (DB::connection('bigquery')->cursor('SELECT * FROM `project.dataset.events`') as $row) { // ... } // See the SQL without running (and paying for) the query $queries = DB::connection('bigquery')->pretend( fn () => UserAnalytics::where('page_views', '>', 100)->get() );
Limitations
These are inherent BigQuery characteristics, not bugs in the package:
- No transactions.
DB::transaction(),beginTransaction(),commit(), androllBack()throwLogicException. BigQuery supports session-scoped transactions but they are not wired up here. - No auto-incrementing primary keys. Assign your own key (ULID/UUID).
insertGetId()throwsLogicExceptionto make this explicit. - DML, not streaming. Inserts, updates, and deletes execute as DML statements and are subject to BigQuery's DML quotas. For high-volume ingestion, use a batch load job or the streaming insert API directly via the underlying
BigQueryClient(DB::connection('bigquery')->getClient()). - No row locking.
lockForUpdate()andsharedLock()throwLogicException. - No joins in
UPDATE/DELETE. BigQuery has no such syntax; both throwLogicException. Use aMERGEstatement viaDB::connection('bigquery')->statement(), or a subquery in theWHEREclause. - No
upsert(). Write aMERGEstatement instead. - No schema builder.
Schema::hasTable(),Schema::create(), and friends throwLogicException. Run DDL withDB::connection('bigquery')->statement(). - No PDO.
getPdo()/getReadPdo()throwLogicException. Code or third-party packages that introspect the underlying PDO will not work. - BigQuery-specific driver. Not interchangeable with other Laravel database drivers.
Upgrading from v2 to v3
v3 fixes identifier quoting and column qualification, which changes the SQL the package emits.
- Identifiers are backtick-quoted per segment.
t.user_idcompiles to`t`.`user_id`, and a table compiles to`project`.`dataset`.`table`rather than`project.dataset.table`. The single-quoted form is one identifier as far as BigQuery is concerned, so its implicit alias is the wholeproject.dataset.tablestring andtable.columndoes not resolve. Identifiers containing backticks now have them stripped rather than being interpolated verbatim, which closes an injection hole inorderBy()/where()when the column name came from request input. - Columns are qualified with the table reference, not the full path.
Model::find(), relations, andwithCount()previously emittedproject.dataset.table.column, which BigQuery rejects. They now emittable.column. get()returnsstdClassrows, matching Laravel's query builder contract, instead of arrays. Eloquent models are unaffected.- BigQuery value objects are unwrapped.
TIMESTAMP,DATE,NUMERIC, andBYTEScolumns hydrate as PHP strings rather thanGoogle\Cloud\BigQuery\*objects, so$castsworks as expected. BigQueryModeldefaults to$incrementing = falseand$keyType = 'string'. Models that already set these are unaffected.delete($id)now scopes to that key instead of ignoring the argument and deleting everything matching the current constraints.- The empty
LaravelBigqueryEloquentclass, its facade, and theLaravelBigqueryEloquentalias were removed. None of them were ever functional. lockForUpdate()andsharedLock()now throw. They previously compiled to nothing and silently returned unlocked rows.update()anddelete()with joins now throw. They previously emitted MySQL-shaped SQL that BigQuery rejects, pointing you atMERGEinstead.BYTEScolumns hydrate as a raw binary string rather than a PSR-7 stream object.- Laravel 11 is no longer supported. Its query grammar has no reference to the connection, so the driver could never resolve
project.datasetfor an unqualified table name on 11. Laravel 11 reached end of life on 2026-03-12.
Testing
Run the test suite with:
composer test
The suite mocks the BigQuery client, so it verifies the SQL the driver emits but not that BigQuery accepts it. A live smoke test covers the rest:
BIGQUERY_SMOKE_PROJECT=your-project composer test-smoke
It creates a throwaway laravel_bq_smoke_* dataset, exercises reads, writes, and the
BigQuery-specific SQL against it, then drops the dataset. Nothing outside that dataset
is written to. It needs a billing-enabled project — the BigQuery sandbox does not
support DML — and scans a few megabytes, well inside the 1 TiB monthly free tier.
Contributing
Contributions are welcome. Please open an issue or a pull request.
Security
If you discover any security vulnerabilities, please report them via our security policy.
Credits
License
This package is open-source software licensed under the MIT License.