A fully-featured Laravel database driver for Snowflake using the REST SQL API. No PHP extensions or ODBC drivers required.
- 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
- PHP 8.2+
- Laravel 12.0+
- Snowflake account with REST API access
composer require foundry-co/laravel-snowflakeThe package will auto-register its service provider.
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.pubThen assign the public key to your Snowflake user:
ALTER USER your_user SET RSA_PUBLIC_KEY='MIIBIjANBgkqh...';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=SYSADMINAdd 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'),
],
],
],
],'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',
],
],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 serveUpdate 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'),
],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'
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();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');
}
};| 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 |
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',
];
}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 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;
}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
}This package defaults to ULIDs for primary keys because they offer significant advantages for Snowflake:
- 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- Use if you need compatibility with UUID-based systems
- Not time-sortable (random distribution)
$table->uuidPrimary();- Snowflake's auto-increment equivalent
- Consider only for compatibility with existing schemas
$table->identity('id', start: 1, increment: 1);composer test- 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
MIT License. See LICENSE for details.