File Coverage

blib/lib/App/Sqitch/Engine/snowflake.pm
Criterion Covered Total %
statement 126 137 91.9
branch 34 40 85.0
condition 17 27 62.9
subroutine 50 53 94.3
pod 12 12 100.0
total 239 269 88.8


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