File Coverage

blib/lib/App/Sqitch/Engine/snowflake.pm
Criterion Covered Total %
statement 109 120 90.8
branch 34 40 85.0
condition 16 27 59.2
subroutine 47 50 94.0
pod 11 11 100.0
total 217 248 87.5


line stmt bran cond sub pod time code
1             package App::Sqitch::Engine::snowflake;
2              
3 2     2   22787 use 5.010;
  2         17  
4 2     2   11 use Moo;
  2         4  
  2         16  
5 2     2   910 use utf8;
  2         6  
  2         14  
6 2     2   49 use Path::Class;
  2         16  
  2         119  
7 2     2   1562 use DBI;
  2         19651  
  2         92  
8 2     2   21 use Try::Tiny;
  2         5  
  2         130  
9 2     2   15 use App::Sqitch::X qw(hurl);
  2         5  
  2         34  
10 2     2   737 use Locale::TextDomain qw(App-Sqitch);
  2         7  
  2         15  
11 2     2   416 use App::Sqitch::Types qw(DBH ArrayRef HashRef URIDB Str);
  2         7  
  2         31  
12              
13             extends 'App::Sqitch::Engine';
14              
15             our $VERSION = 'v1.4.0'; # VERSION
16              
17 4     4 1 15861 sub key { 'snowflake' }
18 3     3 1 52 sub name { 'Snowflake' }
19 2     2 1 10 sub driver { 'DBD::ODBC 1.59' }
20 5     5 1 699 sub default_client { 'snowsql' }
21              
22             sub destination {
23 15     15 1 3196 my $self = shift;
24             # Just use the target name if it doesn't look like a URI.
25 15 100       110 return $self->target->name if $self->target->name !~ /:/;
26              
27             # Use the URI sans password.
28 12         55 my $uri = $self->target->uri->clone;
29 12 100       189 $uri->password(undef) if $uri->password;
30 12         564 return $uri->as_string;
31             }
32              
33             has _snowsql => (
34             is => 'ro',
35             isa => ArrayRef,
36             lazy => 1,
37             default => sub {
38             my $self = shift;
39             my $uri = $self->uri;
40             my @ret = ( $self->client );
41             for my $spec (
42             [ accountname => $self->account ],
43             [ username => $self->username ],
44             [ dbname => $uri->dbname ],
45             [ rolename => $self->role ],
46             ) {
47             push @ret, "--$spec->[0]" => $spec->[1] if $spec->[1];
48             }
49              
50             if (my %vars = $self->variables) {
51             push @ret => map {; '--variable', "$_=$vars{$_}" } sort keys %vars;
52             }
53              
54             push @ret => $self->_client_opts;
55             return \@ret;
56             },
57             );
58              
59 25     25 1 3911 sub snowsql { @{ shift->_snowsql } }
  25         529  
60              
61             has _snowcfg => (
62             is => 'rw',
63             isa => HashRef,
64             lazy => 1,
65             default => sub {
66             my $hd = $^O eq 'MSWin32' && "$]" < '5.016' ? $ENV{HOME} || $ENV{USERPROFILE} : (glob('~'))[0];
67             return {} if not $hd;
68             my $fn = dir $hd, '.snowsql', 'config';
69             return {} unless -e $fn;
70             my $data = App::Sqitch::Config->new->load_file($fn);
71             my $cfg = {};
72             for my $k (keys %{ $data }) {
73             # We only want the default connections config. No named config.
74             # (For now, anyway; maybe use database as config name laster?)
75             next unless $k =~ /\Aconnections[.]([^.]+)\z/;
76             my $key = $1;
77             my $val = $data->{$k};
78             # Apparently snowsql config supports single quotes, while
79             # Config::GitLike does not.
80             # https://support.snowflake.net/s/case/5000Z000010xUYJQA2
81             # https://docs.snowflake.com/en/user-guide/snowsql-config.html#snowsql-config-file
82             if ($val =~ s/\A'//) {
83             $val = $data->{$k} unless $val =~ s/'\z//;
84             }
85             $cfg->{$key} = $val;
86             }
87             return $cfg;
88             },
89             );
90              
91             has uri => (
92             is => 'ro',
93             isa => URIDB,
94             lazy => 1,
95             default => sub {
96             my $self = shift;
97             my $uri = $self->SUPER::uri;
98              
99             # Set defaults in the URI.
100             $uri->host($self->_host($uri));
101             # Use _port instead of port so it's empty if no port is in the URI.
102             # https://github.com/sqitchers/sqitch/issues/675
103             # XXX SNOWSQL_PORT deprecated; remove once Snowflake removes it.
104             $uri->port($ENV{SNOWSQL_PORT}) if !$uri->_port && $ENV{SNOWSQL_PORT};
105             $uri->dbname(
106             $ENV{SNOWSQL_DATABASE}
107             || $self->_snowcfg->{dbname}
108             || $self->username
109             ) if !$uri->dbname;
110             return $uri;
111             },
112             );
113              
114             sub _def_user {
115 7 100 100 7   659 $ENV{SNOWSQL_USER} || $_[0]->_snowcfg->{username} || $_[0]->sqitch->sysuser
116             }
117              
118 4 100   4   390 sub _def_pass { $ENV{SNOWSQL_PWD} || shift->_snowcfg->{password} }
119             sub _def_acct {
120             my $acct = $ENV{SNOWSQL_ACCOUNT} || $_[0]->_snowcfg->{accountname}
121 6   33 6   42 || hurl engine => __('Cannot determine Snowflake account name');
122              
123             # XXX Region is deprecated as a separate value, because the acount name may now be
124             # <account_name>.<region_id>.<cloud_platform_or_private_link>
125             # https://docs.snowflake.com/en/user-guide/snowsql-start.html#a-accountname
126             # Remove from here down and just return on the line above once Snowflake removes it.
127 6 100 66     42 my $region = $ENV{SNOWSQL_REGION} || $_[0]->_snowcfg->{region} or return $acct;
128 1         11 return "$acct.$region";
129             }
130              
131             has account => (
132             is => 'ro',
133             isa => Str,
134             lazy => 1,
135             default => sub {
136             my $self = shift;
137             if (my $host = $self->uri->host) {
138             # <account_name>.<region_id>.<cloud_platform_or_privatelink>.snowflakecomputing.com
139             $host =~ s/[.]snowflakecomputing[.]com$//;
140             return $host;
141             }
142             return $self->_def_acct;
143             },
144             );
145              
146             sub _host {
147 9     9   24 my ($self, $uri) = @_;
148 9 100       37 if (my $host = $uri->host) {
149 2 100       94 return $host if $host =~ /\.snowflakecomputing\.com$/;
150 1         17 return $host . ".snowflakecomputing.com";
151             }
152             # XXX SNOWSQL_HOST is deprecated; remove it once Snowflake removes it.
153 7 100       199 return $ENV{SNOWSQL_HOST} if $ENV{SNOWSQL_HOST};
154 6         24 return $self->_def_acct . '.snowflakecomputing.com';
155             }
156              
157             has warehouse => (
158             is => 'ro',
159             isa => Str,
160             lazy => 1,
161             default => sub {
162             my $self = shift;
163             my $uri = $self->uri;
164             require URI::QueryParam;
165             $uri->query_param('warehouse')
166             || $ENV{SNOWSQL_WAREHOUSE}
167             || $self->_snowcfg->{warehousename}
168             || 'sqitch';
169             },
170             );
171              
172             has role => (
173             is => 'ro',
174             isa => Str,
175             lazy => 1,
176             default => sub {
177             my $self = shift;
178             my $uri = $self->uri;
179             require URI::QueryParam;
180             $uri->query_param('role')
181             || $ENV{SNOWSQL_ROLE}
182             || $self->_snowcfg->{rolename}
183             || '';
184             },
185             );
186              
187             has dbh => (
188             is => 'rw',
189             isa => DBH,
190             lazy => 1,
191             default => sub {
192             my $self = shift;
193             $self->use_driver;
194             my $uri = $self->uri;
195             DBI->connect($uri->dbi_dsn, $self->username, $self->password, {
196             PrintError => 0,
197             RaiseError => 0,
198             AutoCommit => 1,
199             odbc_utf8_on => 1,
200             FetchHashKeyName => 'NAME_lc',
201             HandleError => sub {
202             my ($err, $dbh) = @_;
203             $@ = $err;
204             @_ = ($dbh->state || 'DEV' => $dbh->errstr);
205             goto &hurl;
206             },
207             Callbacks => {
208             connected => sub {
209             my $dbh = shift;
210             my $wh = _quote_ident($dbh, $self->warehouse);
211             my $role = $self->role;
212             $dbh->do($_) or return for (
213             ($role ? ("USE ROLE " . _quote_ident($dbh, $role)) : ()),
214             "ALTER WAREHOUSE $wh RESUME IF SUSPENDED",
215             "USE WAREHOUSE $wh",
216             'ALTER SESSION SET TIMESTAMP_TYPE_MAPPING=TIMESTAMP_LTZ',
217             "ALTER SESSION SET TIMESTAMP_OUTPUT_FORMAT='YYYY-MM-DD HH24:MI:SS'",
218             "ALTER SESSION SET TIMEZONE='UTC'",
219             );
220             $dbh->do('USE SCHEMA ' . _quote_ident($dbh, $self->registry))
221             or $self->_handle_no_registry($dbh);
222             return;
223             },
224             disconnect => sub {
225             my $dbh = shift;
226             my $wh = _quote_ident($dbh, $self->warehouse);
227             $dbh->do("ALTER WAREHOUSE $wh SUSPEND");
228             return;
229             },
230             },
231             });
232             }
233             );
234              
235             sub _quote_ident {
236 14     14   10044 my ($dbh, $ident) = @_;
237             # https://docs.snowflake.com/en/sql-reference/identifiers-syntax
238 14 100       126 return $ident if $ident =~ /^[_a-zA-Z][_a-zA-Z0-9\$]*$/;
239 6 50 33     28 return $ident if $ident =~ /^"/ && $ident =~ /"$/;
240 6         29 return $dbh->quote_identifier($ident);
241             }
242              
243             # Need to wait until dbh is defined.
244             with 'App::Sqitch::Role::DBIEngine';
245              
246             sub _client_opts {
247             return (
248 5     5   102 '--noup',
249             '--option' => 'auto_completion=false',
250             '--option' => 'echo=false',
251             '--option' => 'execution_only=false',
252             '--option' => 'friendly=false',
253             '--option' => 'header=false',
254             '--option' => 'exit_on_error=true',
255             '--option' => 'stop_on_error=true',
256             '--option' => 'output_format=csv',
257             '--option' => 'paging=false',
258             '--option' => 'timing=false',
259             # results=false suppresses errors! Bug report:
260             # https://support.snowflake.net/s/case/5000Z000010wm6BQAQ/
261             '--option' => 'results=true',
262             '--option' => 'wrap=false',
263             '--option' => 'rowset_size=1000',
264             '--option' => 'syntax_style=default',
265             '--option' => 'variable_substitution=true',
266             '--variable' => 'registry=' . $_[0]->registry,
267             '--variable' => 'warehouse=' . $_[0]->warehouse,
268             );
269             }
270              
271             sub _quiet_opts {
272             return (
273 2     2   29 '--option' => 'quiet=true',
274             );
275             }
276              
277             sub _verbose_opts {
278             return (
279 16     16   408 '--option' => 'quiet=false',
280             );
281             }
282              
283             # Not using arrays, but delimited strings that are the default in
284             # App::Sqitch::Role::DBIEngine, because:
285             # * There is currently no literal syntax for arrays
286             # https://support.snowflake.net/s/case/5000Z000010wXBRQA2/
287             # * Scalar variables like the array constructor can't be used in WHERE clauses
288             # https://support.snowflake.net/s/case/5000Z000010wX7yQAE/
289             sub _listagg_format {
290 1     1   1214 return q{listagg(%1$s, ' ') WITHIN GROUP (ORDER BY %1$s)};
291             }
292              
293 1     1   25 sub _ts_default { 'current_timestamp' }
294              
295             sub _initialized {
296 1     1   4 my $self = shift;
297 1         4 return $self->dbh->selectcol_arrayref(q{
298             SELECT true
299             FROM information_schema.tables
300             WHERE TABLE_CATALOG = current_database()
301             AND TABLE_SCHEMA = UPPER(?)
302             AND TABLE_NAME = UPPER(?)
303             }, undef, $self->registry, 'changes')->[0];
304             }
305              
306             sub _initialize {
307 0     0   0 my $self = shift;
308 0         0 my $schema = $self->registry;
309 0 0       0 hurl engine => __x(
310             'Sqitch schema "{schema}" already exists',
311             schema => $schema
312             ) if $self->initialized;
313              
314 0         0 $self->run_file( file(__FILE__)->dir->file('snowflake.sql') );
315 0         0 $self->dbh->do("USE SCHEMA $schema");
316 0         0 $self->_register_release;
317             }
318              
319             sub _no_table_error {
320 4   100 4   76 return $DBI::state && $DBI::state eq '42S02'; # ERRCODE_UNDEFINED_TABLE
321             }
322              
323             sub _no_column_error {
324 3   100 3   43 return $DBI::state && $DBI::state eq '42703'; # ERRCODE_UNDEFINED_COLUMN
325             }
326              
327             sub _unique_error {
328             # https://docs.snowflake.com/en/sql-reference/constraints-overview
329             # Snowflake supports defining and maintaining constraints, but does not
330             # enforce them, except for NOT NULL constraints, which are always enforced.
331 1     1   9 return 0;
332             }
333              
334             sub _ts2char_format {
335             # The colon has to be inside the quotation marks, because otherwise it
336             # generates wayward single quotation marks. Bug report:
337             # https://support.snowflake.net/s/case/5000Z000010wTkKQAU/
338 1     1   9 qq{to_varchar(CONVERT_TIMEZONE('UTC', %s), '"year:"YYYY":month:"MM":day:"DD":hour:"HH24":minute:"MI":second:"SS":time_zone:UTC"')};
339             }
340              
341 1     1   7738 sub _char2ts { $_[1]->as_string(format => 'iso') }
342              
343             sub _dt($) {
344 1     1   9 require App::Sqitch::DateTime;
345 1         75 return App::Sqitch::DateTime->new(split /:/ => shift);
346             }
347              
348 1     1   24 sub _regex_op { 'REGEXP' } # XXX But not used; see regex_expr() below.
349              
350 1     1   37 sub _simple_from { ' FROM dual' }
351              
352             sub _cid {
353 1     1   49 my ( $self, $ord, $offset, $project ) = @_;
354              
355 1 50       6 my $offset_expr = $offset ? " OFFSET $offset" : '';
356             return try {
357 1   0 1   268 $self->dbh->selectcol_arrayref(qq{
358             SELECT change_id
359             FROM changes
360             WHERE project = ?
361             ORDER BY committed_at $ord
362             LIMIT 1$offset_expr
363             }, undef, $project || $self->plan->project)->[0];
364             } catch {
365 1 50 33 1   50 return if $self->_no_table_error && !$self->initialized;
366 1         10 die $_;
367 1         47 };
368             }
369              
370             sub changes_requiring_change {
371 0     0 1 0 my ( $self, $change ) = @_;
372             # NOTE: Query from DBIEngine doesn't work in Snowflake:
373             # SQL compilation error: Unsupported subquery type cannot be evaluated (SQL-42601)
374             # Looks like it doesn't yet support correlated subqueries.
375             # https://docs.snowflake.com/en/sql-reference/operators-subquery.html
376             # The CTE-based query borrowed from Exasol seems to be fine, however.
377 0         0 return @{ $self->dbh->selectall_arrayref(q{
  0         0  
378             WITH tag AS (
379             SELECT tag, committed_at, project,
380             ROW_NUMBER() OVER (partition by project ORDER BY committed_at) AS rnk
381             FROM tags
382             )
383             SELECT c.change_id, c.project, c.change, t.tag AS asof_tag
384             FROM dependencies d
385             JOIN changes c ON c.change_id = d.change_id
386             LEFT JOIN tag t ON t.project = c.project AND t.committed_at >= c.committed_at
387             WHERE d.dependency_id = ?
388             AND (t.rnk IS NULL OR t.rnk = 1)
389             }, { Slice => {} }, $change->id) };
390             }
391              
392             sub name_for_change_id {
393 0     0 1 0 my ( $self, $change_id ) = @_;
394             # NOTE: Query from DBIEngine doesn't work in Snowflake:
395             # SQL compilation error: Unsupported subquery type cannot be evaluated (SQL-42601)
396             # Looks like it doesn't yet support correlated subqueries.
397             # https://docs.snowflake.com/en/sql-reference/operators-subquery.html
398             # The CTE-based query borrowed from Exasol seems to be fine, however.
399 0         0 return $self->dbh->selectcol_arrayref(q{
400             WITH tag AS (
401             SELECT tag, committed_at, project,
402             ROW_NUMBER() OVER (partition by project ORDER BY committed_at) AS rnk
403             FROM tags
404             )
405             SELECT change || COALESCE(t.tag, '@HEAD')
406             FROM changes c
407             LEFT JOIN tag t ON c.project = t.project AND t.committed_at >= c.committed_at
408             WHERE change_id = ?
409             AND (t.rnk IS NULL OR t.rnk = 1)
410             }, undef, $change_id)->[0];
411             }
412              
413             # https://support.snowflake.net/s/question/0D50Z00008BENO5SAP
414 2     2   34 sub _limit_default { '4611686018427387903' }
415              
416             sub _limit_offset {
417             # LIMIT/OFFSET don't support parameters, alas. So just put them in the query.
418 6     6   360 my ($self, $lim, $off) = @_;
419             # OFFSET cannot be used without LIMIT, sadly.
420             # https://support.snowflake.net/s/case/5000Z000010wfnWQAQ
421 6 100 66     54 return ['LIMIT ' . ($lim || $self->_limit_default), "OFFSET $off"], [] if $off;
422 4 100       38 return ["LIMIT $lim"], [] if $lim;
423 2         26 return [], [];
424             }
425              
426             sub _regex_expr {
427 1     1   156 my ( $self, $col, $regex ) = @_;
428             # Snowflake regular expressions are implicitly anchored to match the
429             # entire string. To work around this, issue, we use regexp_substr(), which
430             # is not so anchored, and just check to see that if it returns a string.
431             # https://support.snowflake.net/s/case/5000Z000010wbUSQAY
432             # https://support.snowflake.net/s/question/0D50Z00008C90beSAB/
433 1         16 return "regexp_substr($col, ?) IS NOT NULL", $regex;
434             }
435              
436             sub run_file {
437 1     1 1 5 my ($self, $file) = @_;
438 1         4 $self->_run(_quiet_opts, '--filename' => $file);
439             }
440              
441             sub run_verify {
442 1     1 1 10 my ($self, $file) = @_;
443             # Suppress STDOUT unless we want extra verbosity.
444 1 50       11 return $self->run_file($file) unless $self->sqitch->verbosity > 1;
445 1         11 $self->_run(_verbose_opts, '--filename' => $file);
446             }
447              
448             sub run_handle {
449 1     1 1 4 my ($self, $fh) = @_;
450 1         11 $self->_spool($fh);
451             }
452              
453             sub _run {
454 4     4   990 my $self = shift;
455 4         27 my $sqitch = $self->sqitch;
456 4 100       98 my $pass = $self->password or
457             # Use capture and emit instead of _run to avoid a wayward newline in
458             # the output.
459             return $sqitch->emit_literal( $sqitch->capture( $self->snowsql, @_ ) );
460             # Does not override connection config, alas.
461 1         130 local $ENV{SNOWSQL_PWD} = $pass;
462 1         5 return $sqitch->emit_literal( $sqitch->capture( $self->snowsql, @_ ) );
463             }
464              
465             sub _capture {
466 2     2   84 my $self = shift;
467 2         8 my $sqitch = $self->sqitch;
468 2 100       48 my $pass = $self->password or
469             return $sqitch->capture( $self->snowsql, _verbose_opts, @_ );
470 1         19 local $ENV{SNOWSQL_PWD} = $pass;
471 1         6 return $sqitch->capture( $self->snowsql, _verbose_opts, @_ );
472             }
473              
474             sub _probe {
475 2     2   80 my $self = shift;
476 2         15 my $sqitch = $self->sqitch;
477 2 100       51 my $pass = $self->password or
478             return $sqitch->probe( $self->snowsql, _verbose_opts, @_ );
479 1         30 local $ENV{SNOWSQL_PWD} = $pass;
480 1         7 return $sqitch->probe( $self->snowsql, _verbose_opts, @_ );
481             }
482              
483             sub _spool {
484 3     3   2017 my $self = shift;
485 3         7 my $fh = shift;
486 3         9 my $sqitch = $self->sqitch;
487 3 100       76 my $pass = $self->password or
488             return $sqitch->spool( $fh, $self->snowsql, _verbose_opts, @_ );
489 1         17 local $ENV{SNOWSQL_PWD} = $pass;
490 1         9 return $sqitch->spool( $fh, $self->snowsql, _verbose_opts, @_ );
491             }
492              
493             1;
494              
495             __END__
496              
497             =head1 Name
498              
499             App::Sqitch::Engine::snowflake - Sqitch Snowflake Engine
500              
501             =head1 Synopsis
502              
503             my $snowflake = App::Sqitch::Engine->load( engine => 'snowflake' );
504              
505             =head1 Description
506              
507             App::Sqitch::Engine::snowflake provides the Snowflake storage engine for Sqitch.
508              
509             =head1 Interface
510              
511             =head2 Attributes
512              
513             =head3 C<uri>
514              
515             Returns the Snowflake database URI name. It starts with the URI for the target
516             and builds out missing parts. Sqitch looks for the host name in this order:
517              
518             =over
519              
520             =item 1
521              
522             In the host name of the target URI. If that host name does not end in
523             C<snowflakecomputing.com>, Sqitch appends it. This lets Snowflake URLs just
524             reference the Snowflake account name or the account name and region in URLs.
525              
526             =item 2
527              
528             In the C<$SNOWSQL_HOST> environment variable (Deprecated by Snowflake).
529              
530             =item 3
531              
532             By concatenating the account name and region, if available, from the
533             C<$SNOWSQL_ACCOUNT> environment variable or C<connections.accountname> setting
534             in the
535             L<SnowSQL configuration file|https://docs.snowflake.com/en/user-guide/snowsql-start.html#configuring-default-connection-settings>,
536             the C<$SNOWSQL_REGION> or C<connections.region> setting in the
537             L<SnowSQL configuration file|https://docs.snowflake.com/en/user-guide/snowsql-start.html#configuring-default-connection-settings>,
538             and C<snowflakecomputing.com>. Note that Snowflake has deprecated
539             C<$SNOWSQL_REGION> and C<connections.region>, and will be removed in a future
540             version. Append the region name and cloud platform name to the account name,
541             instead.
542              
543             =back
544              
545             The database name is determined by the following methods:
546              
547             =over
548              
549             =item 1.
550              
551             The path par t of the database URI.
552              
553             =item 2.
554              
555             The C<$SNOWSQL_DATABASE> environment variable.
556              
557             =item 3.
558              
559             In the C<connections.dbname> setting in the
560             L<SnowSQL configuration file|https://docs.snowflake.com/en/user-guide/snowsql-start.html#configuring-default-connection-settings>.
561              
562             =item 4.
563              
564             If sqitch finds no value in the above places, it falls back on the system
565             username.
566              
567             =back
568              
569             Other attributes of the URI are set from the C<account>, C<username> and
570             C<password> attributes documented below.
571              
572             =head3 C<account>
573              
574             Returns the Snowflake account name, or an exception if none can be determined.
575             Sqitch looks for the account code in this order:
576              
577             =over
578              
579             =item 1
580              
581             In the host name of the target URI.
582              
583             =item 2
584              
585             In the C<$SNOWSQL_ACCOUNT> environment variable.
586              
587             =item 3
588              
589             In the C<connections.accountname> setting in the
590             L<SnowSQL configuration file|https://docs.snowflake.com/en/user-guide/snowsql-start.html#configuring-default-connection-settings>.
591              
592             =back
593              
594             =head3 username
595              
596             Returns the snowflake user name. Sqitch looks for the user name in this order:
597              
598             =over
599              
600             =item 1
601              
602             In the C<$SQITCH_USERNAME> environment variable.
603              
604             =item 2
605              
606             In the target URI.
607              
608             =item 3
609              
610             In the C<$SNOWSQL_USER> environment variable.
611              
612             =item 4
613              
614             In the C<connections.username> variable from the
615             L<SnowSQL config file|https://docs.snowflake.com/en/user-guide/snowsql-config.html#snowsql-config-file>.
616              
617             =item 5
618              
619             The system username.
620              
621             =back
622              
623             =head3 password
624              
625             Returns the snowflake password. Sqitch looks for the password in this order:
626              
627             =over
628              
629             =item 1
630              
631             In the C<$SQITCH_PASSWORD> environment variable.
632              
633             =item 2
634              
635             In the target URI.
636              
637             =item 3
638              
639             In the C<$SNOWSQL_PWD> environment variable.
640              
641             =item 4
642              
643             In the C<connections.password> variable from the
644             L<SnowSQL config file|https://docs.snowflake.com/en/user-guide/snowsql-config.html#snowsql-config-file>.
645              
646             =back
647              
648             =head3 C<warehouse>
649              
650             Returns the warehouse to use for all connections. This value will be available
651             to all Snowflake change scripts as the C<&warehouse> variable. Sqitch looks
652             for the warehouse in this order:
653              
654             =over
655              
656             =item 1
657              
658             In the C<warehouse> query parameter of the target URI
659              
660             =item 2
661              
662             In the C<$SNOWSQL_WAREHOUSE> environment variable.
663              
664             =item 3
665              
666             In the C<connections.warehousename> variable from the
667             L<SnowSQL config file|https://docs.snowflake.com/en/user-guide/snowsql-config.html#snowsql-config-file>.
668              
669             =item 4
670              
671             If none of the above are found, it falls back on the hard-coded value
672             "sqitch".
673              
674             =back
675              
676             =head3 C<role>
677              
678             Returns the role to use for all connections. Sqitch looks for the role in this
679             order:
680              
681             =over
682              
683             =item 1
684              
685             In the C<role> query parameter of the target URI
686              
687             =item 2
688              
689             In the C<$SNOWSQL_ROLE> environment variable.
690              
691             =item 3
692              
693             In the C<connections.rolename> variable from the
694             L<SnowSQL config file|https://docs.snowflake.com/en/user-guide/snowsql-config.html#snowsql-config-file>.
695              
696             =item 4
697              
698             If none of the above are found, no role will be set.
699              
700             =back
701              
702             =head2 Instance Methods
703              
704             =head3 C<initialized>
705              
706             $snowflake->initialize unless $snowflake->initialized;
707              
708             Returns true if the database has been initialized for Sqitch, and false if it
709             has not.
710              
711             =head3 C<initialize>
712              
713             $snowflake->initialize;
714              
715             Initializes a database for Sqitch by installing the Sqitch registry schema.
716              
717             =head3 C<snowsql>
718              
719             Returns a list containing the C<snowsql> client and options to be passed to
720             it. Used internally when executing scripts.
721              
722             =head1 Author
723              
724             David E. Wheeler <david@justatheory.com>
725              
726             =head1 License
727              
728             Copyright (c) 2012-2023 iovation Inc., David E. Wheeler
729              
730             Permission is hereby granted, free of charge, to any person obtaining a copy
731             of this software and associated documentation files (the "Software"), to deal
732             in the Software without restriction, including without limitation the rights
733             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
734             copies of the Software, and to permit persons to whom the Software is
735             furnished to do so, subject to the following conditions:
736              
737             The above copyright notice and this permission notice shall be included in all
738             copies or substantial portions of the Software.
739              
740             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
741             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
742             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
743             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
744             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
745             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
746             SOFTWARE.
747              
748             =cut