foundry-co / laravel-snowflake
Snowflake database driver for Laravel using REST API - no PDO extension required
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/foundry-co/laravel-snowflake
Requires
- php: ^8.3
- firebase/php-jwt: ^6.10
- laravel/framework: ^12.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
This package is auto-updated.
Last update: 2025-12-18 04:25:16 UTC
README
A fully-featured Laravel database driver for Snowflake using the REST SQL API. No PHP extensions or ODBC drivers required.
Features
- Pure PHP Implementation - Uses Snowflake's REST API, no
pdo_snowflakeor ODBC required - Full Eloquent Support - Models, relationships, and all Eloquent features work seamlessly
- Laravel Query Builder - Complete query builder support with Snowflake-specific SQL
- Migrations - Full schema builder with Snowflake-specific column types
- ULID Primary Keys - Time-sortable, distributed-safe IDs optimized for Snowflake clustering
- Semi-Structured Data - Native support for VARIANT, OBJECT, and ARRAY types
- JWT & OAuth Authentication - Secure authentication with key-pair or OAuth tokens
- Large Result Sets - Efficient partition-based streaming for large query results
Requirements
- PHP 8.2+
- Laravel 12.0+
- Snowflake account with REST API access
Installation
composer require foundry-co/laravel-snowflake
The package will auto-register its service provider.
Configuration
1. Snowflake Account Setup
Before using this package, you need to set up key-pair authentication in Snowflake:
# Generate a private key openssl genrsa 2048 | openssl pkcs8 -topk8 -inform PEM -out snowflake_key.p8 -nocrypt # Extract the public key openssl rsa -in snowflake_key.p8 -pubout -out snowflake_key.pub
Then assign the public key to your Snowflake user:
ALTER USER your_user SET RSA_PUBLIC_KEY='MIIBIjANBgkqh...';
2. Environment Variables
Add these to your .env file:
SNOWFLAKE_ACCOUNT=your-account-identifier SNOWFLAKE_WAREHOUSE=COMPUTE_WH SNOWFLAKE_DATABASE=MY_DATABASE SNOWFLAKE_SCHEMA=PUBLIC SNOWFLAKE_USER=your_username SNOWFLAKE_PRIVATE_KEY_PATH=/path/to/snowflake_key.p8 SNOWFLAKE_ROLE=SYSADMIN
3. Database Configuration
Add the Snowflake connection to config/database.php:
'connections' => [ 'snowflake' => [ 'driver' => 'snowflake', 'account' => env('SNOWFLAKE_ACCOUNT'), 'warehouse' => env('SNOWFLAKE_WAREHOUSE'), 'database' => env('SNOWFLAKE_DATABASE'), 'schema' => env('SNOWFLAKE_SCHEMA', 'PUBLIC'), 'role' => env('SNOWFLAKE_ROLE'), 'prefix' => '', 'auth' => [ 'method' => 'jwt', 'jwt' => [ 'user' => env('SNOWFLAKE_USER'), 'private_key_path' => env('SNOWFLAKE_PRIVATE_KEY_PATH'), 'private_key_passphrase' => env('SNOWFLAKE_PRIVATE_KEY_PASSPHRASE'), ], ], ], ],
OAuth Authentication (Alternative)
'auth' => [ 'method' => 'oauth', 'oauth' => [ 'token_endpoint' => env('SNOWFLAKE_OAUTH_TOKEN_ENDPOINT'), 'client_id' => env('SNOWFLAKE_OAUTH_CLIENT_ID'), 'client_secret' => env('SNOWFLAKE_OAUTH_CLIENT_SECRET'), 'scope' => 'session:role-any', ], ],
Using 1Password or Secrets Managers
Instead of storing your private key in a file, you can provide the key content directly. This is useful when using 1Password CLI, HashiCorp Vault, or other secrets managers.
# Use private_key instead of private_key_path SNOWFLAKE_PRIVATE_KEY="-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEF... -----END PRIVATE KEY-----"
With 1Password CLI, you can reference secrets directly:
SNOWFLAKE_PRIVATE_KEY="op://vault/snowflake/private-key"
Then run your application with:
op run -- php artisan serve
Update your database configuration to use the key content:
'jwt' => [ 'user' => env('SNOWFLAKE_USER'), 'private_key' => env('SNOWFLAKE_PRIVATE_KEY'), // Content instead of path 'private_key_passphrase' => env('SNOWFLAKE_PRIVATE_KEY_PASSPHRASE'), ],
Usage
Eloquent Models
Add the UsesSnowflake trait to any model that connects to Snowflake:
use Illuminate\Database\Eloquent\Model; use FoundryCo\Snowflake\Eloquent\Concerns\UsesSnowflake; class User extends Model { use UsesSnowflake; protected $connection = 'snowflake'; protected $table = 'users'; protected $fillable = ['name', 'email']; }
The trait automatically:
- Generates ULID primary keys (time-sortable, optimal for Snowflake clustering)
- Handles Snowflake timestamp formats with microsecond precision
- Sets
$incrementing = falseand$keyType = 'string'
Query Builder
Use the query builder as you normally would:
// Basic queries $users = DB::connection('snowflake')->table('users')->get(); // Inserts (ULID will be generated if id not provided) DB::connection('snowflake')->table('users')->insert([ 'id' => Str::ulid()->toLower(), 'name' => 'John Doe', 'email' => 'john@example.com', ]); // Updates DB::connection('snowflake') ->table('users') ->where('id', $id) ->update(['name' => 'Jane Doe']); // JSON/Variant queries DB::connection('snowflake') ->table('events') ->where('payload->type', 'purchase') ->get();
Migrations
Create migrations with Snowflake-specific features:
use Illuminate\Database\Migrations\Migration; use FoundryCo\Snowflake\Schema\SnowflakeBlueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { protected $connection = 'snowflake'; public function up(): void { Schema::connection('snowflake')->create('users', function (SnowflakeBlueprint $table) { // ULID primary key (recommended) $table->id(); // Standard columns $table->string('name'); $table->string('email')->unique(); // Snowflake semi-structured types $table->variant('preferences'); // VARIANT - any JSON data $table->object('metadata'); // OBJECT - key-value pairs $table->array('tags'); // ARRAY - ordered list // Snowflake timestamp types $table->timestampNtz('processed_at'); // No timezone $table->timestampLtz('local_time'); // Local timezone $table->timestampTz('event_time'); // With timezone // Geospatial types $table->geography('location'); $table->geometry('shape'); // High-precision numbers $table->number('balance', 18, 4); // Standard timestamps $table->timestamps(); // Clustering for query performance $table->clusterBy(['created_at', 'id']); }); } public function down(): void { Schema::connection('snowflake')->dropIfExists('users'); } };
Available Column Types
| Method | Snowflake Type | Description |
|---|---|---|
id() |
CHAR(26) |
ULID primary key |
ulidPrimary() |
CHAR(26) |
ULID primary key |
uuidPrimary() |
VARCHAR(36) |
UUID primary key |
variant() |
VARIANT |
Semi-structured data |
object() |
OBJECT |
Key-value pairs |
array() |
ARRAY |
Ordered list |
geography() |
GEOGRAPHY |
Spherical coordinates |
geometry() |
GEOMETRY |
Planar coordinates |
timestampNtz() |
TIMESTAMP_NTZ |
Timestamp without timezone |
timestampLtz() |
TIMESTAMP_LTZ |
Timestamp in local timezone |
timestampTz() |
TIMESTAMP_TZ |
Timestamp with timezone |
number() |
NUMBER(p,s) |
High-precision decimal |
identity() |
INTEGER IDENTITY |
Auto-incrementing integer |
Custom Casts
Use the included casts for proper type handling:
use Illuminate\Database\Eloquent\Model; use FoundryCo\Snowflake\Casts\VariantCast; use FoundryCo\Snowflake\Casts\SnowflakeTimestamp; use FoundryCo\Snowflake\Eloquent\Concerns\UsesSnowflake; class Event extends Model { use UsesSnowflake; protected $connection = 'snowflake'; protected $casts = [ 'payload' => VariantCast::class, 'occurred_at' => SnowflakeTimestamp::class, 'scheduled_at' => SnowflakeTimestamp::class.':tz', ]; }
Warehouse & Role Switching
Switch context at runtime:
// Get the connection $connection = DB::connection('snowflake'); // Switch warehouse $connection->useWarehouse('ANALYTICS_WH'); // Switch role $connection->useRole('ANALYST'); // Switch schema $connection->useSchema('STAGING'); // Chain methods $results = $connection ->useWarehouse('LARGE_WH') ->useRole('ADMIN') ->table('big_table') ->get();
Transactions
Transactions work as expected:
DB::connection('snowflake')->transaction(function ($db) { $db->table('accounts')->where('id', 1)->decrement('balance', 100); $db->table('accounts')->where('id', 2)->increment('balance', 100); }); // Or manually DB::connection('snowflake')->beginTransaction(); try { // ... operations DB::connection('snowflake')->commit(); } catch (\Exception $e) { DB::connection('snowflake')->rollBack(); throw $e; }
Cursors for Large Results
Use cursors to efficiently process large result sets:
foreach (DB::connection('snowflake')->table('events')->cursor() as $event) { // Process one row at a time // Results are fetched partition by partition }
Primary Keys: ULID vs UUID vs Identity
This package defaults to ULIDs for primary keys because they offer significant advantages for Snowflake:
ULID (Recommended)
- Time-sortable: ULIDs are lexicographically sortable by creation time
- Clustering benefit: Records created near each other are stored near each other
- Client-generated: No round-trip to database needed
- Distributed-safe: No sequence contention
// Default behavior $table->id(); // Creates CHAR(26) ULID column
UUID (Alternative)
- Use if you need compatibility with UUID-based systems
- Not time-sortable (random distribution)
$table->uuidPrimary();
Identity (Legacy)
- Snowflake's auto-increment equivalent
- Consider only for compatibility with existing schemas
$table->identity('id', start: 1, increment: 1);
Testing
composer test
Limitations
- No savepoints: Snowflake doesn't support savepoints
- No row locking: Snowflake is append-only
- No traditional indexes: Use clustering keys instead
- REST API only: All queries go through the REST API
License
MIT License. See LICENSE for details.