Skip to content

Commit

Permalink
Skip registry creation on Snowflake when necessary
Browse files Browse the repository at this point in the history
In some cases, the Sqitch user will lack permission to create a schema,
such as when the DBA doesn't want to give it that permission. Implement
`run_upgrade` in the Snowflake engine to detect this lack of permission
by executing `CREATE SCHEMA IF NOT EXISTS` and stripping out `CREATE
SCHEMA` and `COMMENT ON SCHEMA` commands when it fails due to
insufficient privileges. Resolves #828.
  • Loading branch information
theory committed Jan 3, 2025
1 parent 93a0d1f commit 33291bd
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 4 deletions.
4 changes: 4 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ Revision history for Perl extension App::Sqitch
- Updated the MySQL engine to omit the `checkit()` function when using
binary logging and Sqitch lacks super user permissions. Thanks to Scott
Edwards for the report and to Janosch Peters for the solution (#824).
- Taught the Snowflake engine to detect when the Sqitch user lacks
permission to create a schema and to skip the creation of the registry
schema. Useful for cases when the registry schema was created in
advance. Thanks to Peter Wimsey for the suggestion (#826).

1.4.1 2024-02-04T16:35:32Z
- Removed the quoting of the role and warehouse identifiers that was
Expand Down
44 changes: 42 additions & 2 deletions lib/App/Sqitch/Engine/snowflake.pm
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ has dbh => (
"ALTER SESSION SET TIMESTAMP_OUTPUT_FORMAT='YYYY-MM-DD HH24:MI:SS'",
"ALTER SESSION SET TIMEZONE='UTC'",
);
$dbh->do('USE SCHEMA ' . $self->registry)
$dbh->do('USE SCHEMA ' . $dbh->quote_identifier($self->registry))
or $self->_handle_no_registry($dbh);
return;
},
Expand Down Expand Up @@ -299,11 +299,51 @@ sub _initialize {
schema => $schema
) if $self->initialized;

$self->run_file( file(__FILE__)->dir->file('snowflake.sql') );
$self->run_upgrade(file(__FILE__)->dir->file('snowflake.sql') );
$self->dbh->do("USE SCHEMA $schema");
$self->_register_release;
}

# Determines whether we can create a schema and, if so, executes $file.
# Otherwise creates and executes a copy of $file with schema commands removed.
sub run_upgrade {
my ($self, $file) = @_;
try {
# Can we create a schema?
my $dbh = $self->dbh;
my $reg = $dbh->quote_identifier($self->registry);
$dbh->do("CREATE SCHEMA IF NOT EXISTS $reg");
} catch {
die $_ unless $DBI::state && $DBI::state eq '42501'; # ERRCODE_INSUFFICIENT_PRIVILEGE
# Cannot create schema; strip out schema stuff.
$file = $self->_strip_file($file);
};

# All good.
return $self->run_file($file);
}

# Creates and returns a copy of $file with C<CREATE SCHEMA> and C<COMMENT ON
# SCHEMA> commands stripped out.
sub _strip_file {
my ($self, $file) = @_;
my $in = $file->open('<:raw') or hurl io => __x(
'Cannot open {file}: {error}',
file => $file,
error => $!
);

require File::Temp;
my $out = File::Temp->new;
while (<$in>) {
s/C(?:REATE|OMMENT ON) SCHEMA\b.+//;
print {$out} $_;
}

close $out;
return $out;
}

sub _no_table_error {
return $DBI::state && $DBI::state eq '42S02'; # ERRCODE_UNDEFINED_TABLE
}
Expand Down
45 changes: 43 additions & 2 deletions t/snowflake.t
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/perl -w

# To test against a live Snowflake database, you must set the
# SQITCH_TEST_SNOWFLAKE_URI environment variable. this is a stanard URI::db URI,
# and should look something like this:
# SQITCH_TEST_SNOWFLAKE_URI environment variable. this is a standard URI::db
# URI, and should look something like this:
#
# export SQITCH_TEST_SNOWFLAKE_URI=db:snowflake://username:password@accountname/dbname?Driver=Snowflake;warehouse=warehouse
#
Expand All @@ -15,6 +15,8 @@ use 5.010;
use Test::More 0.94;
use Test::MockModule;
use Test::Exception;
use Test::File::Contents qw(file_contents_unlike);
use DBD::Mem;
use Locale::TextDomain qw(App-Sqitch);
use Capture::Tiny 0.12 qw(:all);
use File::Temp 'tempdir';
Expand Down Expand Up @@ -515,6 +517,45 @@ ok my $now = App::Sqitch::DateTime->now, 'Construct a datetime object';
is $snow->_char2ts($now), $now->as_string(format => 'iso'),
'Should get ISO output from _char2ts';

##############################################################################
# Test run_upgrade.
UPGRADE: {
my $file;
$mock_snow->mock(run_file => sub { $file = $_[1] });

# Need to mock the database handle.
my $dbh = DBI->connect('dbi:Mem:', undef, undef, {});
$mock_snow->mock(dbh => $dbh);
my $mock_dbh = Test::MockModule->new(ref $dbh, no_auto => 1);
my (@do, $do_err);
$mock_dbh->mock(do => sub { shift; @do = @_; die $do_err if $do_err });

# Deploy with schema permissions.
my $fn = file($INC{'App/Sqitch/Engine/snowflake.pm'})->dir->file('snowflake.sql');
ok $snow->run_upgrade($fn), 'Run upgrade';
is $file, $fn, 'Should have executed the unmodified file';

# Deploy withouit schema permissions.
$do_err = 'Ooops';
local *DBI::state;
$DBI::state = '42501';
ok $snow->run_upgrade($fn), 'Run upgrade again';
isnt $file, $fn, 'Should have a modified file';

# Make sure no schema stuff remains in the file.
file_contents_unlike $file, qr/CREATE SCHEMA/,
'Should have no CREATE SCHEMA';
file_contents_unlike $file, qr/COMMENT ON SCHEMA/,
'Should have no COMMENT ON SCHEMA';

# Die with any other error.
$DBI::state = '10030';
throws_ok { $snow->run_upgrade($fn) } qr/Ooops/,
'Should have any other error';
$mock_snow->unmock('run_file');
$mock_snow->unmock('dbh');
}

##############################################################################
# Can we do live tests?
my $dbh;
Expand Down

0 comments on commit 33291bd

Please sign in to comment.