laravel-gtm/snowflake-sdk
A Laravel package that integrates the Snowflake SDK, making it easier to connect to Snowflake and run queries from PHP applications.
Install the SDK and publish its config:
composer require laravel-gtm/snowflake-sdk
php artisan vendor:publish --tag=snowflake-config
Add credentials to .env (account, user, password/key pair, database, schema, warehouse, role) and confirm they map onto the keys in config/snowflake.php. Keep this connection separate from config/database.php unless you have deliberately registered Snowflake as a Laravel connection.
First use case: prove connectivity with a trivial round-trip before wiring anything into the app.
use LaravelGtm\SnowflakeSdk\Facades\Snowflake;
$rows = Snowflake::select('select current_version() as version');
If you get rows back, the account identifier, auth method, and warehouse are all correct — which is the majority of setup failures in one shot.
Look first at: the published config file (every setting the package actually reads is there), then the facade/service class for the query methods (select, statement/unscoped writes, and whatever transaction helper exists). Those three things cover ~90% of day-to-day use.
Decide early whether you are using this for reads only (the common Laravel pattern: app writes to MySQL/Postgres, Snowflake is the analytics sink/queries) or for writes into Snowflake. Reads-only is far simpler and avoids most of the pitfalls below.
Wrap it in a repository/service, not in controllers. The Snowflake client is a thin transport; the query logic belongs behind an interface so it is mockable and so you can swap in a cached/queued implementation later.
final class RevenueReport
{
public function __construct(private SnowflakeClient $client) {}
public function forMonth(CarbonImmutable $month): array
{
return $this->client->select(
'select region, sum(amount) as total
from analytics.revenue
where month = ?
group by region',
[$month->format('Y-m-01')]
);
}
}
Bind values; never interpolate. Wherever the package accepts a bindings array, use it. Where it does not, treat literal construction as a security-sensitive operation (see Gotchas — v0.2.1 is a direct consequence of string-built literals).
Use it in queued jobs and console commands. Snowflake queries are slow by OLTP standards (seconds, not milliseconds) and warehouses bill per second of uptime. Anything user-facing should dispatch a job, write results to your own database or cache, and let the UI read local data. A SnowflakeQueryJob with $timeout, $tries, and a failed() handler that logs the SQL + bindings is a good default shape.
Batch inserts into a single multi-row INSERT rather than looping. Round-trip latency dominates; one statement with N value tuples is dramatically cheaper than N statements. Chunk to a few thousand rows to stay under statement-size limits.
Schedule heavy work off-peak and let auto-suspend do its job. Do not set an aggressive warehouse override per query; pick the warehouse in config and only override for known-heavy jobs. If the package exposes session parameters, set STATEMENT_TIMEOUT_IN_SECONDS and QUERY_TAG (tag your job name — it makes cost attribution in ACCOUNT_USAGE possible, which is the single most useful operational habit with Snowflake).
Integrate with Laravel's cache for expensive, stable aggregates:
$totals = Cache::remember("rev:{$month->format('Y-m')}", now()->addHour(),
fn () => $report->forMonth($month));
Testing: bind a fake client in the container that returns fixture arrays. Do not let the test suite touch a real account — connection setup alone will make CI slow and flaky.
String literal escaping — read the v0.2.1 changelog. v0.2.1 fixes backslashes not being escaped before quotes when the SDK builds Snowflake SQL literals. Practical consequences:
\ can corrupt the generated SQL — a value like C:\temp\ plus the SDK's quote escaping historically produces a dangling escape that either errors or, worse, changes the literal boundaries. Upgrade; this is not a cosmetic patch.\, \', '', \n, and a bare '. Assert the round-trip returns the original string.Account identifier format is the classic first-day trap. xy12345.eu-west-1 vs xy12345.eu-west-1.aws vs an org-account form — the wrong shape gives a confusing auth or host-resolution error rather than "bad account". Copy it verbatim from the Snowflake UI's connection snippet.
Auth: password auth is easiest to start with, but key-pair auth is what you want for anything long-lived. Watch for private-key formatting (PEM headers, newlines preserved, passphrase handling) — .env multi-line values are a frequent source of silent auth failure.
Timezones and types. Snowflake TIMESTAMP_NTZ vs TIMESTAMP_LTZ and DATE handling will bite you when comparing against Carbon values. Normalise explicitly (to_utc(), format('Y-m-d H:i:s')) rather than relying on driver coercion, and remember returned numerics may arrive as strings for NUMBER/DECIMAL columns.
Result shape is not Eloquent. Expect arrays or stdClass objects, not models. There is no query builder here — if you find yourself wanting ->where(), you want an Eloquent model on a local database instead.
Error handling and retries. Distinguish transient failures (network, warehouse resuming) from permanent ones (SQL compilation errors, permissions). Retry the former with backoff in the job; never retry the latter — a syntax error will burn warehouse time on every attempt. Log the SQL and bindings on failure, but scrub anything sensitive.
Cost/observability. Set QUERY_TAG per job, monitor ACCOUNT_USAGE.QUERY_HISTORY for slow or repeated queries, and keep warehouses small with short auto-suspend. An accidental full-table scan in a nightly job is the usual way this package generates a surprise bill.
Extension points. If the package's client is bound via the container, prefer rebinding/decorating it (a logging decorator that records duration and row count per query is cheap and pays for itself immediately) over forking. Likewise keep config in config/snowflake.php so per-environment warehouses and roles are deployment concerns, not code changes.
Version pinning. At v0.2.x the surface is still moving and the changelog shows correctness fixes in SQL generation. Pin to ^0.2.1 at minimum and read each patch note before bumping — escaping changes are exactly the class of fix you do not want to discover mid-incident.
How can I help you explore Laravel packages today?