<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Snowflake in the Carolinas</title>
  <subtitle>Random thoughts on all things Snowflake in the Carolinas</subtitle>
  <link href="https://snowflake.pavlik.us/feed.xml" rel="self"/>
  <link href="https://snowflake.pavlik.us/"/>
  <updated>2025-04-23T00:00:00Z</updated>
  <id>https://snowflake.pavlik.us/</id>
  <author>
    <name>Greg Pavlik</name>
  </author>
  <entry>
    <title>Quick Sample of Fuzzy Matching in Snowflake</title>
    <link href="https://snowflake.pavlik.us/index.php/2025/04/23/quick-sample-of-fuzzy-matching-in-snowflake/"/>
    <updated>2025-04-23T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2025/04/23/quick-sample-of-fuzzy-matching-in-snowflake/</id>
    <content type="html">&lt;p&gt;&lt;strong&gt;Quick Sample of Fuzzy Matching in Snowflake&lt;/strong&gt;&lt;/p&gt;
&lt;h3 class=&quot;wp-block-heading&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;This post walks through a quick, practical example of fuzzy name matching using Snowflake SQL. The goal is to identify approximate matches based on phonetic similarity and spelling distance. We&#39;ll progressively build up a simple pattern using &lt;code&gt;SOUNDEX&lt;/code&gt; for fast phonetic filtering and &lt;code&gt;EDITDISTANCE&lt;/code&gt; for final scoring. This isn&#39;t a production-ready pipeline—it—s a conceptual starting point. A more advanced post will follow with a normalized nickname mapping approach.&lt;/p&gt;
&lt;h3 class=&quot;wp-block-heading&quot;&gt;Step 1: Input Parameters&lt;/h3&gt;
&lt;p&gt;We begin by setting the target name to match against. These could be passed in dynamically or used in ad hoc analysis:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;SET FIRST_NAME = &#39;Greg&#39;;
SET LAST_NAME  = &#39;Smith&#39;;
&lt;/pre&gt;
&lt;h3 class=&quot;wp-block-heading&quot;&gt;Step 2: Sample Data&lt;/h3&gt;
&lt;p&gt;Next, we define a small set of names with intentional variations—nicknames, spelling shifts, and common soundalikes. This is your test data:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;WITH NAMES AS (
    SELECT COLUMN1 AS FIRST_NAME, COLUMN2 AS LAST_NAME FROM (
        VALUES 
            (&#39;Greg&#39;, &#39;Smith&#39;),
            (&#39;Gray&#39;, &#39;Smith&#39;),
            (&#39;Greg&#39;, &#39;Smyth&#39;),
            (&#39;Craig&#39;, &#39;Smythe&#39;),
            (&#39;Gregory&#39;, &#39;Smithe&#39;),
            (&#39;Mike&#39;, &#39;Smith&#39;),
            (&#39;Gregg&#39;, &#39;Smith&#39;),
            (&#39;Gregg&#39;, &#39;Smithe&#39;)
    )
),
&lt;/pre&gt;
&lt;h3 class=&quot;wp-block-heading&quot;&gt;Step 3: Phonetic Projection with SOUNDEX&lt;/h3&gt;
&lt;p&gt;We calculate the phonetic representation of first and last names using &lt;code&gt;SOUNDEX&lt;/code&gt;. This lets us filter out clearly unrelated candidates before calculating edit distance:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;SOUNDEX_PROJECTION AS (
    SELECT   FIRST_NAME,
             LAST_NAME,
             SOUNDEX(FIRST_NAME) AS SOUNDEX_FIRST,
             SOUNDEX(LAST_NAME)  AS SOUNDEX_LAST
    FROM NAMES
)
&lt;/pre&gt;
&lt;h3 class=&quot;wp-block-heading&quot;&gt;Step 4: Match by Edit Distance&lt;/h3&gt;
&lt;p&gt;Finally, we compare names that share a soundex prefix, ranking them by &lt;code&gt;EDITDISTANCE&lt;/code&gt; from the target name:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;SELECT    FIRST_NAME,
          LAST_NAME,
          EDITDISTANCE(CONCAT(FIRST_NAME, &#39; &#39;, LAST_NAME),
                       CONCAT($FIRST_NAME, &#39; &#39;, $LAST_NAME)) AS DISTANCE
FROM      SOUNDEX_PROJECTION
WHERE     SOUNDEX_FIRST = SOUNDEX($FIRST_NAME)
      AND SOUNDEX_LAST  = SOUNDEX($LAST_NAME)
      AND EDITDISTANCE(CONCAT(FIRST_NAME, &#39; &#39;, LAST_NAME),
                       CONCAT($FIRST_NAME, &#39; &#39;, $LAST_NAME)) &amp;lt;= 10
ORDER BY  DISTANCE ASC;
&lt;/pre&gt;
&lt;p&gt;This query filters and scores results, favoring names with both phonetic and lexical similarity. You can tune the distance threshold based on your precision-recall tradeoff.&lt;/p&gt;
&lt;h3 class=&quot;wp-block-heading&quot;&gt;Final: Minimal Reproducible Example&lt;/h3&gt;
&lt;p&gt;Here—s the entire working example in one block for easy copy-paste and experimentation:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;SET FIRST_NAME = &#39;Greg&#39;;
SET LAST_NAME  = &#39;Smith&#39;;

WITH NAMES AS (
    SELECT COLUMN1 AS FIRST_NAME, COLUMN2 AS LAST_NAME FROM (
        VALUES 
            (&#39;Greg&#39;, &#39;Smith&#39;),
            (&#39;Gray&#39;, &#39;Smith&#39;),
            (&#39;Greg&#39;, &#39;Smyth&#39;),
            (&#39;Craig&#39;, &#39;Smythe&#39;),
            (&#39;Gregory&#39;, &#39;Smithe&#39;),
            (&#39;Mike&#39;, &#39;Smith&#39;),
            (&#39;Gregg&#39;, &#39;Smith&#39;),
            (&#39;Gregg&#39;, &#39;Smithe&#39;)
    )
),
SOUNDEX_PROJECTION AS (
    SELECT   FIRST_NAME,
             LAST_NAME,
             SOUNDEX(FIRST_NAME) AS SOUNDEX_FIRST,
             SOUNDEX(LAST_NAME)  AS SOUNDEX_LAST
    FROM NAMES
)
SELECT    FIRST_NAME,
          LAST_NAME,
          EDITDISTANCE(CONCAT(FIRST_NAME, &#39; &#39;, LAST_NAME),
                       CONCAT($FIRST_NAME, &#39; &#39;, $LAST_NAME)) AS DISTANCE
FROM      SOUNDEX_PROJECTION
WHERE     SOUNDEX_FIRST = SOUNDEX($FIRST_NAME)
      AND SOUNDEX_LAST  = SOUNDEX($LAST_NAME)
      AND EDITDISTANCE(CONCAT(FIRST_NAME, &#39; &#39;, LAST_NAME),
                       CONCAT($FIRST_NAME, &#39; &#39;, $LAST_NAME)) &amp;lt;= 10
ORDER BY  DISTANCE ASC;
&lt;/pre&gt;
&lt;p&gt;A follow-up article will extend this logic using a normalized mapping table (e.g., mapping &lt;code&gt;Catherine&lt;/code&gt; to &lt;code&gt;Cat&lt;/code&gt;, &lt;code&gt;Katie&lt;/code&gt;, etc.) for formal/informal name handling.&lt;/p&gt;
&lt;p&gt;Stay tuned.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Converting camelCase and SNAKE_CASE</title>
    <link href="https://snowflake.pavlik.us/index.php/2022/06/23/converting-camelcase-and-snake_case/"/>
    <updated>2022-06-23T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2022/06/23/converting-camelcase-and-snake_case/</id>
    <content type="html">&lt;p&gt;In programming and string matching, use of camelCase and SNAKE_CASE are common. Here are two simple Snowflake UDFs to convert between the two.&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;create or replace function camelToSnake(&quot;s&quot; string)
returns string
language sql
as
$$
    upper(regexp_replace(s,&#39;([A-Z])&#39;, &#39;_&#92;&#92;1&#39;, 2))
$$;

select camelToSnake(&#39;quickBrownFox&#39;);

create or replace function snakeToCamel(&quot;s&quot; string)
returns string
language javascript
strict immutable
as
$$
const snakeToCamel = str =&gt;
  str.toLowerCase().replace(/([-_][a-z])/g, group =&gt; group
      .toUpperCase()
      .replace(&#39;-&#39;, &#39;&#39;)
      .replace(&#39;_&#39;, &#39;&#39;)
  );
return snakeToCamel(s);
$$;

select snakeToCamel(&#39;QUICK_BROWN_FOX&#39;);&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Running Dynamic SQL in Snowflake</title>
    <link href="https://snowflake.pavlik.us/index.php/2021/01/22/running-dynamic-sql-in-snowflake/"/>
    <updated>2021-01-22T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2021/01/22/running-dynamic-sql-in-snowflake/</id>
    <content type="html">&lt;h4&gt;Use Cases for Dynamic SQL&lt;/h4&gt;
&lt;p&gt;Dynamic SQL allows you to create and manipulate a string, and then run the resulting string as a SQL statement. Snowflake supports dynamic SQL using the &lt;a href=&quot;https://docs.snowflake.com/en/sql-reference/identifier-literal.html#string-literals-session-variables-bind-variables-as-identifiers&quot; target=&quot;_blank&quot; rel=&quot;noreferrer noopener&quot;&gt;identifier&lt;/a&gt; keyword and &lt;a href=&quot;https://docs.snowflake.com/en/sql-reference/literals-table.html#table-literals&quot; target=&quot;_blank&quot; rel=&quot;noreferrer noopener&quot;&gt;table()&lt;/a&gt; function. There are some notable exceptions; however, where the Snowflake SQL parser currently does not support dynamic SQL.&lt;/p&gt;
&lt;p&gt;For example, suppose you want to unload data to stage on a daily basis. To keep the data organized, you decide to put the unloaded data into paths with the current date, like this:&lt;/p&gt;
&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;@my_stage/2021-01-22/data.csv  -- Data unloaded daily, organized by date
@my_stage/2021-01-23/data.csv&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To accomplish this, you want a single line of SQL to run on a daily basis with the date dynamically generated. The problem is the path in a stage is a string literal that currently does not support using the identifier keyword, variables, or other dynamic SQL methods.&lt;/p&gt;
&lt;h4&gt;Snowflake Stored Procedures for Dynamic SQL&lt;/h4&gt;
&lt;p&gt;Writing a stored procedure is one option to run dynamic SQL along these lines. External calls from something like Python or Java can generate and run dynamic SQL. One disadvantage of external code to run dynamic SQL is that it requires external dependencies to schedule and run the code. Stored procedures elimination of any external dependencies offers a major advantage. Because Snowflake tasks also require no external dependencies, it&#39;s possible schedule and run dynamic SQL with no external dependencies.&lt;/p&gt;
&lt;p&gt;There are two options for stored procedures to run dynamic SQL. One option is to build the SQL statement inside the stored procedure code. While this approach has advantages, it has a major disadvantage. It requires the creation and maintenance of a new stored procedure for each dynamic SQL statement to run. The other approach is to generate SQL statements outside the stored procedure that a single general-purpose stored procedure runs.&lt;/p&gt;
&lt;p&gt;That is the approach the following stored procedure uses. It&#39;s intended to run general-purpose dynamic SQL generated outside the procedure and passed in as a parameter. &lt;/p&gt;
&lt;p&gt;For example in the previously cited example, this allows running the daily data offload like this:&lt;/p&gt;
&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;call run_dynamic_sql(&#39;copy into @mystage/&#39; || current_date || 
     &#39;/data.csv from SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.NATION file_format = (type = CSV)&#39;);&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;A General-Purpose Stored Procedure to Run Dynamic SQL&lt;/h4&gt;
&lt;p&gt;This stored procedure will run any SQL statement that can be run in a Snowflake stored procedure. The procedure will return a JSON object, either an error indication of a JSON document containing the rows from the execution. Since Snowflake JSON documents have a 16 Mb limit, the stored procedure should return only small result sets. Although intended to execute non-query statements, because it returns a JSON you can use it to convert select query results to JSON.&lt;/p&gt;
&lt;figure class=&quot;wp-block-image size-large is-style-default&quot;&gt;&lt;a href=&quot;https://snowflake.pavlik.us/wp-content/uploads/2021/01/Snowflake_Run_Dynamic_SQL.txt&quot;&gt;&lt;img src=&quot;https://snowflake.pavlik.us/wp-content/uploads/2021/01/DownloadSQL.png&quot; alt=&quot;&quot; class=&quot;wp-image-479&quot; /&gt;&lt;/a&gt;&lt;/figure&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;create or replace procedure RUN_DYNAMIC_SQL(&quot;sqlStatement&quot; string)
returns variant
language javascript
execute as caller
as
$$

class Query{
    constructor(statement){
        this.statement = statement;
    }
}

var out = {};
var query = getQuery(sqlStatement);
if (query.error == null) {
    return rsToJSON(query);
} else {
    return {&quot;error&quot;: query.error};
}

function rsToJSON(query) {
    var i;
    var row = {};
    var table = [];
    while (query.resultSet.next()) {
        for(col = 1; col &amp;lt;= query.statement.getColumnCount(); col++) {
            row[query.statement.getColumnName(col)] = query.resultSet.getColumnValue(col);
        }
        table.push(row);
    }
    return table;
}

function getQuery(sql){
    var cmd = {sqlText: sql};
    var query = new Query(snowflake.createStatement(cmd));
    try {
        query.resultSet = query.statement.execute();
    } catch (e) {
        query.error = e.message;
    }
    return query;
}
$$;

-- Usage samples:

-- Create a table. Note the use of $$ to define strings to avoid problems with single quotes
call run_dynamic_sql($$ create or replace temp table foo(v variant) $$);

-- Run a select statement. While this SP is intended to execute non queries, it will also
-- Return a query&#39;s result set as a JSON as long as the JSON is under 16 MB in size.
call run_dynamic_sql($$ select * from &quot;SNOWFLAKE_SAMPLE_DATA&quot;.&quot;TPCH_SF1&quot;.&quot;NATION&quot; $$);

-- Show an example copying into a dynamically-named path in a stage:
-- Create a scratch stage for the test
create or replace stage my_stage;

-- Set a variable for the path
set today = current_date;

select $today;

-- Build the copy command. Use &#39; or $$ to terminate strings as convenient.
set copycommand = &#39;copy into @my_stage/&#39; || $TODAY || &#39;/data.csv&#39; ||
$$ from &quot;SNOWFLAKE_SAMPLE_DATA&quot;.&quot;TPCH_SF1&quot;.&quot;NATION&quot; file_format = (type = CSV, field_optionally_enclosed_by = &#39;&quot;&#39;) $$;

-- Examine the statement to make sure it looks okay
select $copycommand;

-- Copy the file to the dynamic path
call run_dynamic_sql($copycommand);

create stage mystage;
call run_dynamic_sql(&#39;copy into @mystage/&#39; || current_date || &#39;/data.csv from SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.NATION file_format = (type = CSV)&#39;);
&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Getting Snowflake&#39;s Current Timezone</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/12/10/getting-snowflakes-current-timezone/"/>
    <updated>2020-12-10T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/12/10/getting-snowflakes-current-timezone/</id>
    <content type="html">&lt;p&gt;Snowflake&#39;s built-in way to get the current timezone is using the SHOW statement, like this:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;show parameters like &#39;TIMEZONE&#39;;&lt;/pre&gt;
&lt;p&gt;If you&#39;re running the statement from a command line, this isn&#39;t a problem. If you need it programmatically, the SHOW command has two key limitations. First, you can&#39;t use it in a stored procedure. Second, if you use it in a SQL script running externally, you have to get the results of the SHOW statement in second query using RESULT_SCAN. &lt;/p&gt;
&lt;p&gt;One of my customers needed a way to get and current timezone in a single call and use it in a stored procedure. Since JavaScript has built-in functions, it&#39;s possible to use a UDF to get the current time zone that way:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;create or replace function GET_CURRENT_TIMEZONE()
returns string
language javascript
as
$$
    return Intl.DateTimeFormat().resolvedOptions().timeZone;
$$;

-- Test the UDF:
select get_current_timezone();
alter session set TIMEZONE = &#39;America/Chicago&#39;;
select get_current_timezone();
alter session set TIMEZONE = &#39;America/New_York&#39;;
select get_current_timezone();&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Geolocation of IP Addresses in Snowflake — Part 3</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/11/30/geolocation-of-ip-addresses-in-snowflake-part-3/"/>
    <updated>2020-11-30T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/11/30/geolocation-of-ip-addresses-in-snowflake-part-3/</id>
    <content type="html">&lt;h4&gt;Programming Note&lt;/h4&gt;
&lt;p&gt;This is a continuation of the &lt;a href=&quot;https://snowflake.pavlik.us/index.php/2019/05/30/geolocation-of-ip-addresses-in-snowflake/&quot;&gt;Part 1&lt;/a&gt; and &lt;a href=&quot;https://snowflake.pavlik.us/index.php/2019/07/12/geolocation-of-ip-addresses-in-snowflake-part-2/&quot;&gt;Part 2&lt;/a&gt; of this series. Since considerable time has passed and changes made to the testing since posting those articles, this post will start from the beginning.&lt;/p&gt;
&lt;h4&gt;The Business Case for Geolocating IP Numbers&lt;/h4&gt;
&lt;p&gt;Business intelligence and data science teams can get valuable insights knowing the geolocation of website visitors. Suppose a product launch gets lots of web traffic, but the only source of information on visitors is the web log. Some web server statistics report on traffic grouped by nation, but what if we want to get much more granular information and incorporate this information with the main data warehouse?&lt;/p&gt;
&lt;h4&gt;The First Step - Getting Web Visitor IP Numbers&lt;/h4&gt;
&lt;p&gt;Let—s take one sample web server among many, Apache Web Server, and quickly examine the structure of a log entry. Here—s a line in a sample Apache Web Server log.&lt;/p&gt;
&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;64.242.88.10 - - &amp;#91;07/Mar/2004:16:05:49 -0800] &quot;GET /twiki/bin/edit/Main/Double_bounce_sender?topicparent=Main.ConfigurationVariables HTTP/1.1&quot; 401 12846&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the Apache documentation, we can get more detailed information on the meaning of each field in the line, but for now we—re going to concentrate on 1) how to load the web logs into Snowflake, and 2) the key aspects for business intelligence and geolocation.&lt;/p&gt;
&lt;h4&gt;Importing Web Log Data&lt;/h4&gt;
&lt;p&gt;Loading the data is a quick proposition. Even without reading the Apache documentation it—s clear that the web log is space delimited and wraps any fields with spaces inside double quotes. Snowflake provides a very simple way to ingest structured data in flat files using File Formats. You can create a file format using SnowSQL (documented here:&amp;nbsp;&lt;a href=&quot;https://docs.snowflake.net/manuals/sql-reference/sql/create-file-format.html&quot;&gt;https://docs.snowflake.net/manuals/sql-reference/sql/create-file-format.html&lt;/a&gt;) or you can use the Snowflake Web UI (documented here:&amp;nbsp;&lt;a href=&quot;https://docs.snowflake.net/manuals/user-guide/data-load-web-ui.html#step-1-open-the-load-data-wizard&quot;&gt;https://docs.snowflake.net/manuals/user-guide/data-load-web-ui.html#step-1-open-the-load-data-wizard&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Note: Although the Apache Web Log is space delimited, we will use the CSV option for the Snowflake File Format — simply change the delimiter from a comma to a space.&lt;/p&gt;
&lt;p&gt;Note: For this exercise, we&#39;ll use a database named WEBLOG. The Apache Web Server data will go into a schema named APACHE_WEB_SERVER, and the geolocating data will go in a schema named IP2LOCATION. You can change the sample code as necessary if you&#39;d prefer to use another database or different schema names.  &lt;/p&gt;
&lt;p&gt;Here is the Snowflake File Format you can use to import Apache Web Server logs:&lt;/p&gt;
&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;ALTER FILE FORMAT &quot;WEBLOG&quot;.&quot;APACHE_WEB_SERVER&quot;.APACHE_WEB_LOG SET COMPRESSION = &#39;AUTO&#39;
FIELD_DELIMITER = &#39;,&#39; RECORD_DELIMITER = &#39;&#92;n&#39; SKIP_HEADER = 1 
FIELD_OPTIONALLY_ENCLOSED_BY = &#39;&#92;042&#39; TRIM_SPACE = FALSE ERROR_ON_COLUMN_COUNT_MISMATCH = FALSE
ESCAPE = &#39;NONE&#39; ESCAPE_UNENCLOSED_FIELD = &#39;&#92;134&#39; DATE_FORMAT = &#39;AUTO&#39;
TIMESTAMP_FORMAT = &#39;AUTO&#39; NULL_IF = (&#39;&#39;);&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Where to Get Sample Apache Web Server Logs&lt;/h4&gt;
&lt;p&gt;One of the thing needed to test geolocating a web server log is, well, a web server log. As it turns out, this is not an easy thing to do. After some research, I located a partially obfuscated Apache web log from the US Securities and Exchange Commission (SEC) available here:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.sec.gov/dera/data/edgar-log-file-data-set.html&quot;&gt;https://www.sec.gov/dera/data/edgar-log-file-data-set.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Their weblogs obfuscate the final quad of the IPv4 dotted quad, so they look like this: 192.168.1.jjr.&lt;/p&gt;
&lt;p&gt;After loading the data, we now need to geolocate the web hits. Problem: the Apache Web Server log, as most web logs, does not show anything for geolocation. Fortunately in most cases, we can use the visitor—s IP address to get fairly accurate geolocation.&lt;/p&gt;
&lt;p&gt;Later in this article, we&#39;ll show how to convert the partially obfuscated IP dotted quad for use in geolocation.&lt;/p&gt;
&lt;h4&gt;Where to Get IP Number Geolocation Databases&lt;/h4&gt;
&lt;p&gt;Third party services keep up to date databases of IPv4 and IPv6 geolocation data. Once such service I found at??&lt;a href=&quot;https://lite.ip2location.com/&quot;&gt;https://lite.ip2location.com&lt;/a&gt;??includes free databases with less rich information than the paid versions. In my testing I found the free databases accurate and useful, though production BI or data science work should consider the paid versions.&lt;/p&gt;
&lt;p&gt;Another source available through Snowflake&#39;s Data Marketplace is &lt;a href=&quot;https://ipinfo.io/&quot;&gt;ipinfo.io&lt;/a&gt;. I have not tested their IP geolocation data source, but the schema is similar to the one provided by IP2Location and the two should work similar in this article. One advantage of using Snowflake&#39;s Data Marketplace is that the partner keeps the data up to date using Snowflake data sharing.&lt;/p&gt;
&lt;p&gt;If you use the IP2Location free database, here is a Snowflake File Format to import the data:&lt;/p&gt;
&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;ALTER FILE FORMAT &quot;WEBLOG&quot;.&quot;IP2LOCATION&quot;.IP2LOCATION SET COMPRESSION = &#39;AUTO&#39; 
FIELD_DELIMITER = &#39;,&#39; RECORD_DELIMITER = &#39;&#92;n&#39; SKIP_HEADER = 0 
FIELD_OPTIONALLY_ENCLOSED_BY = &#39;&#92;042&#39; TRIM_SPACE = TRUE ERROR_ON_COLUMN_COUNT_MISMATCH = TRUE
ESCAPE = &#39;NONE&#39; ESCAPE_UNENCLOSED_FIELD = &#39;&#92;134&#39; DATE_FORMAT = &#39;AUTO&#39;
TIMESTAMP_FORMAT = &#39;AUTO&#39; NULL_IF = (&#39;&#92;&#92;N&#39;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After copying the file to a stage named IP2LOCATION, You can then copy the data into a table using this COPY INTO statement:&lt;/p&gt;
&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;create table IP_TO_LOCATION as
select  $1::int as START_IP,
        $2::int as END_IP,
        $3::string as ISO_COUNTRY,
        $4::string as COUNTRY,
        $5::string as STATE_PROVINCE,
        $6::string as CITY,
        $7::double as LATITUDE,
        $8::double as LONGITUDE,
        $9::string as POSTAL_CODE,
        $10::string as TZ_OFFSET
from @IP2LOCATION (file_format =&gt; &#39;IP2LOCATION&#39;);&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Preparing the Data&lt;/h4&gt;
&lt;p&gt;The next question is how to resolve millions or billions of web log lines— IP address to approximate geolocation. This is where Snowflake shines. The IP2Location LITE comes as a flat structured file with millions of rows After creating another Snowflake File Format, it—s an easy matter to turn the IP2Location flat file into a Snowflake table. From there, Snowflake—s powerful massive-scale join make it a simple matter to create a joined view that shows the IP—s approximate location.&lt;/p&gt;
&lt;p&gt;Before using the geolocation data, there&#39;s a data preparation item to get it working. The IP2Location data comes with IPs represented by 32-bit integers, not the familiar dotted quad notation. This makes it much easier to use code and database routines that search for ranges of IPs that all happen to be in the same area by specifying a lower and upper range for the IP number.&lt;/p&gt;
&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;IP Dotted Quad:     IP 32-bit Integer, Decimal
192.168.1.1         3232235777&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means we need to convert our dotted quad values into 32-bit integer values. Fortunately, Snowflake makes that easy with a UDF (User Defined Function):&lt;/p&gt;
&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;--Convert an IPv4 Dotted Quad into an IP Number
create or replace function IpToInteger(ipAddress varchar)
  returns double 
  language javascript
  strict
  as &#39;
     
    var ipQuad = IPADDRESS.split(&quot;.&quot;);
  
    var quad1 = parseInt(ipQuad&amp;#91;0]);
    var quad2 = parseInt(ipQuad&amp;#91;1]);
    var quad3 = parseInt(ipQuad&amp;#91;2]);
    var quad4 = parseInt(ipQuad&amp;#91;3]);
    return (quad1 * 16777216) + (quad2 * 65536) + (quad3 * 256) + quad4;
 
  &#39;;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Simulating the Partially Obfuscated IP Number:&lt;/h4&gt;
&lt;p&gt;If you don&#39;t have your own web logs and are using the ones mentioned in this article from the SEC, they obfuscate the final quad of the IPv4 dotted quad, so they look like this: 192.168.1.jjr.&lt;/p&gt;
&lt;p&gt;According to the SEC&#39;s documentation, their method will always replace the final number from 0 to 255 with the same three-letter value. The SEC does not disclose that —jjr— maps to something like 134, but for the purposes of this test it—s acceptable to assign all 256 unique three letter replacements with numbers from 0 to 255.&lt;/p&gt;
&lt;p&gt;After downloading the log file named —log20170630.zip—, we need to load it to a stage and use a file format to parse it. Here is the file format I used:&lt;/p&gt;
&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;ALTER FILE FORMAT &quot;TEST&quot;.&quot;IP2LOCATION&quot;.IP_LOG SET COMPRESSION = &#39;AUTO&#39; FIELD_DELIMITER = &#39;,&#39;
RECORD_DELIMITER = &#39;&#92;n&#39; SKIP_HEADER = 1 FIELD_OPTIONALLY_ENCLOSED_BY = &#39;NONE&#39; TRIM_SPACE = TRUE
ERROR_ON_COLUMN_COUNT_MISMATCH = TRUE ESCAPE = &#39;NONE&#39; ESCAPE_UNENCLOSED_FIELD = &#39;&#92;134&#39;
DATE_FORMAT = &#39;AUTO&#39; TIMESTAMP_FORMAT = &#39;AUTO&#39; NULL_IF = (&#39;&#92;N&#39;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After loading the data into a table (I called mine IP_TO_LOCATION in the WEBLOG database and IP2LOCATION schema), the next thing to do is replace the three-letter obfuscated replacements with numbers from 0 through 255. To do this, I ran an ELT process replace these values using these steps:&lt;/p&gt;
&lt;p&gt;Step 1: Create a new table with all the columns from the original web log, plus a new column to hold the last three characters of the IP number (the three-letter replacement).&lt;/p&gt;
&lt;p&gt;Step 2: Create a table with each of the distinct values in the new column. There will be 256 representing numbers from 0 through 255. For the purposes of this exercise, it is not important which numbers map to which three-letter replacements, so I assigned them in sequential order from 0 through 255 in the order returned in the select distinct query. I called these IP numbers simulated, even though only the final quad of the dotted quads is simulated.&lt;/p&gt;
&lt;p&gt;Step 3: Convert the IP dotted quads into IP numbers using the UDF in Part 1 of this series. The SQL looks like this:&lt;/p&gt;
&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;create table WEB_LOG_ENRICHED as
(
select IpToInteger(SIMULATED_IP) as IP_NUMBER,
IP, SIMULATED_QUAD, SIMULATED_IP, REQUEST_DATE, REQUEST_TIME, ZONE, CIK, ACCESSION,
EXTENSION, CODE, SIZE, IDX, NOREFER, NOAGENT, FIND, CRAWLER, BROWSER 
from IP_LOG) -- Note: IP_LOG holds the raw Apache Web Server logs&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Simulating the IP Numbers at Scale &lt;/h4&gt;
&lt;p&gt;I was concerned about performance running a Javascript UDF millions of 20,488,579 times to convert IP dotted quads into IP numbers. It turned out I needn—t have been concerned. Snowflake converted all 20.5 million in 16.97 seconds using an Extra Small (XS) warehouse. Out of curiosity, I dropped the resulting table and increased the size of the warehouse to a medium (M) and it ran in 7.85 seconds. This provides empirical evidence that increasing the warehouse size improves performance including those with UDFs.&lt;/p&gt;
&lt;p&gt;This led me to the final part of the experiment — resolving the IP numbers in the web log to the geolocations. We have the IP numbers in the a log table, and the IP geolocations in another. My first thought was to join the tables, but this is an unusual join. The standard join matches keys. In this example, we need to use inequalities to join the table. In other words, join the IP log information with the geolocation data where the IP log—s IP number falls between the lower and upper bounds of a locations address range.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;Geolocating the Web Hits&lt;/h4&gt;
&lt;p&gt;This leads to the final part of the experiment — resolving the IP numbers in the web log to the geolocations. We have the IP numbers in the a log table, and the IP geolocations in another. My first thought was to join the tables, but this is an unusual join. The standard join matches keys. In this example, we need to use inequalities to join the table. In other words, join the IP log information with the geolocation data where the IP log—s IP number falls between the lower and upper bounds of a locations address range.&lt;/p&gt;
&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;select * from &quot;WEBLOG&quot;.&quot;APACHE_WEB_SERVER&quot;.&quot;WEB_LOG_ENRICHED&quot; W
left join &quot;WEBLOG&quot;.&quot;IP2LOCATION&quot;.&quot;IP_TO_LOCATION&quot; L
on W.IP_INT &gt;= L.START_IP and W.IP_INT &amp;lt;= L.END_IP limit 100;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While this works, the performance was not practical for at scale usage. In this test, geocoding 100 web log entries on an extra small warehouse (single node) took about 30 seconds. That simply won&#39;t scale.&lt;/p&gt;
&lt;h4&gt;Performance Tuning the Geolocation&lt;/h4&gt;
&lt;p&gt;In most cases the first step in improving a Snowflake query is examining the query profiler. Here&#39;s the query profile for the first test run of the geolocation:&lt;/p&gt;
&lt;div class=&quot;wp-block-image is-style-default&quot;&gt;&lt;figure class=&quot;aligncenter size-large is-resized&quot;&gt;&lt;img src=&quot;https://snowflake.pavlik.us/wp-content/uploads/2020/11/IP2Location_LeftJoin-1024x904.jpg&quot; alt=&quot;Query Profile on Cartesian Join&quot; class=&quot;wp-image-455&quot; width=&quot;489&quot; height=&quot;431&quot; /&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The left outer join seems to be the culprit here. It turns out Snowflake&#39;s optimizer doesn&#39;t particularly like doing a left outer join on an inequality. The interesting thing about that is this should be a one to one relationship. For each IP number in the web log, there should be exactly one row where that IP number falls into the right range.&lt;/p&gt;
&lt;p&gt;This allows us to do a cartesian join instead:&lt;/p&gt;
&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;select * from &quot;WEBLOG&quot;.&quot;APACHE_WEB_SERVER&quot;.&quot;WEB_LOG_ENRICHED&quot; W
 inner join &quot;WEBLOG&quot;.&quot;IP2LOCATION&quot;.&quot;IP_TO_LOCATION&quot; L
    on W.IP_INT &gt;= L.START_IP and W.IP_INT &amp;lt;= L.END_IP limit 500000; &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This leads to a query profile like this:&lt;/p&gt;
&lt;div class=&quot;wp-block-image&quot;&gt;&lt;figure class=&quot;aligncenter size-large is-resized&quot;&gt;&lt;img src=&quot;https://snowflake.pavlik.us/wp-content/uploads/2020/11/IP2Location_CartesianJoin-1024x890.jpg&quot; alt=&quot;Query Profile of CartesianJoin&quot; class=&quot;wp-image-458&quot; width=&quot;550&quot; height=&quot;478&quot; /&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;More important, it leads to performance like this on an extra small warehouse:  &lt;/p&gt;
&lt;p&gt;After running an initial query that took a few seconds and got the critical IP geolocation table into the cache, geolocating half a million web log entries took just five seconds.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Least Privilege Access to Monitor Snowflake Usage</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/11/25/least-privilege-access-to-monitor-snowflake-usage/"/>
    <updated>2020-11-25T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/11/25/least-privilege-access-to-monitor-snowflake-usage/</id>
    <content type="html">&lt;h4&gt;The SNOWFLAKE Database&lt;/h4&gt;
&lt;p&gt;All Snowflake accounts should have a database named SNOWFLAKE. It&#39;s a shared database, using Snowflake&#39;s &lt;a href=&quot;https://docs.snowflake.com/en/user-guide/data-sharing-intro.html&quot; target=&quot;_blank&quot; rel=&quot;noreferrer noopener&quot;&gt;secure data sharing&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you set up your Snowflake account before the spring of 2019, you may need to &lt;a href=&quot;https://docs.snowflake.com/en/user-guide/data-sharing-provider.html#web-interface-for-shares&quot; target=&quot;_blank&quot; rel=&quot;noreferrer noopener&quot;&gt;import the shared database&lt;/a&gt;. If you don&#39;t see the SNOWFLAKE database in your account and don&#39;t see it as an inbound share, contact Snowflake Support. &lt;/p&gt;
&lt;p&gt;Snowflake, Inc. (the company) unsurprisingly runs a number of its own Snowflake accounts. The SNOWFLAKE database in your account is an inbound share from the Snowflake, Inc. account running on your cloud provider and region.&lt;/p&gt;
&lt;p&gt;Because the SNOWFLAKE database contains information on usage and metering, by default only the ACCOUNTADMIN role has privileges to select on the views.&lt;/p&gt;
&lt;h4&gt;Attempting Grants on the SNOWFLAKE Database&lt;/h4&gt;
&lt;p&gt;Without this background, it&#39;s possible to conclude improperly that only the ACCOUNTADMIN can access the views in the SNOWFLAKE database. Here&#39;s why:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;-- A user may try something like this:
use role ACCOUNTADMIN;
grant select on &quot;SNOWFLAKE&quot;.&quot;ACCOUNT_USAGE&quot;.&quot;QUERY_HISTORY&quot; to role SYSADMIN;&lt;/pre&gt;
&lt;p&gt;This will result in the error &lt;code&gt;&lt;span class=&quot;has-inline-color has-vivid-red-color&quot;&gt;Grant not executed: Insufficient privileges.&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The ACCOUNTADMIN is the &lt;a href=&quot;https://docs.snowflake.com/en/user-guide/security-access-control-considerations.html#control-the-assignment-of-the-accountadmin-role-to-users&quot; target=&quot;_blank&quot; rel=&quot;noreferrer noopener&quot;&gt;most powerful role&lt;/a&gt; in a Snowflake account.  Because of this, a person may conclude that there&#39;s no way to grant privileges on the &quot;special&quot; SNOWFLAKE database.&lt;/p&gt;
&lt;h4&gt;Granting Privileges on SNOWFLAKE to Human Users&lt;/h4&gt;
&lt;p&gt;As previously discussed, the only thing special about the SNOWFLAKE database is it&#39;s an inbound shared database. You &lt;em&gt;&lt;strong&gt;can&lt;/strong&gt;&lt;/em&gt; grant access to the SNOWFLAKE database the same way you do for any other inbound shared database:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;use role ACCOUNTADMIN;
grant imported privileges on database SNOWFLAKE to role SYSADMIN;
use role SYSADMIN; -- Remember to always get out of the ACCOUNTADMIN role when done using it.&lt;/pre&gt;
&lt;p&gt;This will grant select on all views in the SNOWFLAKE database to the SYSADMIN role. It&#39;s up to your organization whether or not you want to grant this access. Personally, I recommend it. It allows users with SYSADMIN role but not ACCOUNTADMIN role to monitor usage. It also allows users with ACCOUNTADMIN role to use least privilege to access this information without changing roles to ACCOUNTADMIN. Any time a user changes to ACCOUNTADMIN it&#39;s possible to forget to get out of that role. That risks performing other actions such as creating object that generally shouldn&#39;t be done in that role.&lt;/p&gt;
&lt;h4&gt;Granting Privileges on SNOWFLAKE to Machine Users&lt;/h4&gt;
&lt;p&gt;If granting access to the SNOWFLAKE database is for a dashboard or machine user, you can do something like this:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;-- You must use the ACCOUNTADMIN role to assign the required privileges
use role ACCOUNTADMIN;
 
-- Optionally create a warehouse to monitor Snowflake activity.
-- Only create a warehouse dedicated to account usage if there are several
-- clients (performance monitors, BI packages, etc.) using it.
create or replace warehouse MONITOR_SNOWFLAKE warehouse_size = &#39;X-Small&#39;;
 
-- Create a new role intended to monitor Snowflake usage.
create or replace role MONITOR_SNOWFLAKE;
 
-- Grant privileges on the SNOWFLAKE database to the new role.
grant imported privileges on database SNOWFLAKE to role MONITOR_SNOWFLAKE;
 
-- Create a user.
create or replace user    SNOWFLAKE_MONITOR
    LOGIN_NAME          = SNOWFLAKE_MONITOR
    password            = &#39;My_Password_123!&#39;
    default_warehouse   = MONITOR_SNOWFLAKE
    default_role        = MONITOR_SNOWFLAKE
    default_namespace   = SNOWFLAKE.ACCOUNT_USAGE
--  rsa_public_key      = &#39;MIIBIjANBgkqh...&#39; -- Optional, see note.
;
 
-- Note: Snowflake recommends using key authentication for machine users:
-- https://docs.snowflake.com/en/user-guide/snowsql-start.html#using-key-pair-authentication
 
-- Grant usage on the warehouse used to monitor Snowflake.
grant usage on warehouse MONITOR_SNOWFLAKE to role MONITOR_SNOWFLAKE;
 
-- Grant the monitor role to the user.
grant role MONITOR_SNOWFLAKE to user SNOWFLAKE_MONITOR;
 
-- Get out of the ACCOUNTADMIN role when done.
use role SYSADMIN;&lt;/pre&gt;
&lt;h4&gt;Increasing Granularity of the Grants&lt;/h4&gt;
&lt;p&gt;Let&#39;s discuss more granular access to the views in an imported shared database. Privileges on the imported shared database itself are all or nothing. If you want to control access with more granularity, create a set of &quot;select * from...&quot; views selecting from the SNOWFLAKE database. You can then manage the grants on those views individually.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Snowflake Version</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/11/24/snowflake-version/"/>
    <updated>2020-11-24T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/11/24/snowflake-version/</id>
    <content type="html">&lt;p&gt;One of the great things about being a Snowflake customer is you&#39;ll never have to perform upgrades and patches. Snowflake performs upgrades and patches for you, transparently with no down time or degraded performance.&lt;/p&gt;
&lt;p&gt;Upgrades and patches happen so seamlessly, I suspect most customers would have no idea what release of the Snowflake platform they&#39;re currently running. I know I don&#39;t. That&#39;s a good thing. Old features keep working as Snowflake enables new features on an ongoing basis. &lt;/p&gt;
&lt;p&gt;There are times when it&#39;s useful to know what version of Snowflake you&#39;re running. You can check what version of Snowflake you&#39;re running using the CURRENT_VERSION() function:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;select CURRENT_VERSION();&lt;/pre&gt;
&lt;p&gt;At the time of this writing, the current version of Snowflake is 4.39.5. There are situations where it&#39;s useful to know when Snowflake performed upgrades and patches over time. For example, if you have a SQL statement that you didn&#39;t change and it performed differently than before, one explanation could be a change of Snowflake version.&lt;/p&gt;
&lt;p&gt;You can check your Snowflake version over time running this statement:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;select      to_date(min(START_TIME))  as START_DATE,
            to_date(max(START_TIME))  as END_DATE,
            RELEASE_VERSION           as SNOWFLAKE_VERSION
from        &quot;SNOWFLAKE&quot;.&quot;ACCOUNT_USAGE&quot;.&quot;QUERY_HISTORY&quot;
group by    RELEASE_VERSION
having      START_DATE &gt;= current_date - 30   -- Check upgrades for previous 30 days 
order by    START_DATE desc;&lt;/pre&gt;
&lt;p&gt;If you see a change in version between the times your regularly run statement executed differently, that could be one possibility. To help confirm that, you can go to the query history in the Snowflake UI and examine the query profiles for each query. If they look the same, the version upgrade isn&#39;t the explanation for the difference. If they look different, the version upgrade could be a possible reason.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Regex Non-Capturing Groups and Lookarounds in Snowflake</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/11/20/regex-non-capturing-groups-and-lookarounds-in-snowflake/"/>
    <updated>2020-11-20T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/11/20/regex-non-capturing-groups-and-lookarounds-in-snowflake/</id>
    <content type="html">&lt;p&gt;If you don&#39;t need the background or discussion of how they work and just want to download Snowflake UDFs that support regex non-capturing groups, lookaheads, and lookbehinds, you can download them here:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/GregPavlik/SnowflakeUDFs/tree/main/RegularExpressions&quot;&gt;https://github.com/GregPavlik/SnowflakeUDFs/tree/main/RegularExpressions&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now for the background:&lt;/p&gt;
&lt;p&gt;Snowflake supports regular expressions (&lt;a href=&quot;https://en.wikipedia.org/wiki/Regular_expression#:~:text=The%20concept%20arose%20in%20the,description%20of%20a%20regular%20language.&quot;&gt;regex&lt;/a&gt;) for &lt;a href=&quot;https://docs.snowflake.com/en/sql-reference/functions/rlike.html&quot;&gt;string matching&lt;/a&gt; and &lt;a href=&quot;https://docs.snowflake.com/en/sql-reference/functions/regexp_replace.html&quot;&gt;replacements&lt;/a&gt;. If your regex skills are like mine, Snowflake&#39;s regex implementation provides more than you&#39;ll ever need.&lt;/p&gt;
&lt;p&gt;For regex ninjas and people who want to use regular expression libraries, there are two commonly-used capabilities that &lt;a href=&quot;https://community.snowflake.com/s/question/0D50Z00007ENLKsSAP/expanded-support-for-regular-expressions-regex&quot;&gt;this post&lt;/a&gt; explains Snowflake&#39;s regex functions do not currently support: &lt;a href=&quot;https://stackoverflow.com/questions/3512471/what-is-a-non-capturing-group-in-regular-expressions&quot;&gt;non-capturing groups&lt;/a&gt; and &lt;a href=&quot;https://stackoverflow.com/questions/2973436/regex-lookahead-lookbehind-and-atomic-groups&quot;&gt;lookarounds&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;Every once in a while I run into a customer who&#39;s a regex ninja or wants to use a regex from a library that requires one of these capabilities.&lt;/p&gt;
&lt;p&gt;It occurred to me that JavaScript supports regex with these features, and Snowflake supports JavaScript user defined functions (UDFs). To use a regex in Snowflake that has non-capturing groups or lookarounds, It&#39;s a simple matter of writing a UDF.&lt;/p&gt;
&lt;p&gt;The problem is writing a new UDF for each use of a regex reduces some of the main advantages of regular expressions including compactness and simplicity. &lt;/p&gt;
&lt;p&gt;This lead me to write two general-purpose UDFs that approximate Snowflake&#39;s &lt;a href=&quot;https://docs.snowflake.com/en/sql-reference/functions/regexp_replace.html&quot;&gt;REGEXP_REPLACE&lt;/a&gt; and &lt;a href=&quot;https://docs.snowflake.com/en/sql-reference/functions/rlike.html&quot;&gt;RLIKE&lt;/a&gt; (synonym &lt;a href=&quot;https://docs.snowflake.com/en/sql-reference/functions/regexp_like.html&quot;&gt;REGEXP_LIKE&lt;/a&gt;) as closely as possible while enabling non-capturing groups and lookarounds.&lt;/p&gt;
&lt;p&gt;I named the JavaScript UDFs similar to the Snowflake functions they approximate, REGEXP_REPLACE2 and RLIKE2 (synonym REGEXP_LIKE2). I also &lt;a href=&quot;https://community.snowflake.com/s/article/Overloading-JavaScript-UDFs-in-Snowflake&quot;&gt;overloaded the UDFs&lt;/a&gt; so that you can call them using minimal parameters or optional parameters the same as their base Snowflake functions.&lt;/p&gt;
&lt;p&gt;Here&#39;s an example of their usage:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;-- Running the base function returns this error:
-- Invalid regular expression: &#39;bar(?=bar)&#39;, no argument for repetition operator: ?
select regexp_replace(&#39;foobarbarfoo&#39;, &#39;bar(?=bar)&#39;, &#39;***&#39;);

-- Running the UDF approximating the base function returns foo***barfoo
select regexp_replace2(&#39;foobarbarfoo&#39;, &#39;bar(?=bar)&#39;, &#39;***&#39;);

-- Running the base function returns this error:
-- Invalid regular expression: &#39;bar(?=bar)&#39;, no argument for repetition operator: ?
select rlike(&#39;foobarbarfoo&#39;, &#39;bar(?=bar)&#39;);

-- Running the UDF approximating the base function returns TRUE
select rlike2(&#39;foobarbarfoo&#39;, &#39;bar(?=bar)&#39;);&lt;/pre&gt;
&lt;p&gt;You can download the UDFs on my Github here: &lt;a href=&quot;https://github.com/GregPavlik/SnowflakeUDFs/tree/main/RegularExpressions&quot;&gt;https://github.com/GregPavlik/SnowflakeUDFs/tree/main/RegularExpressions&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Getting Snowflake Primary Key Columns as a Table</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/07/29/getting-snowflake-primary-key-columns-as-a-table/"/>
    <updated>2020-07-29T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/07/29/getting-snowflake-primary-key-columns-as-a-table/</id>
    <content type="html">&lt;p&gt;One of my customers had an interesting requirement. In order to dynamically create merge statements, they needed a way to collect the primary key columns for any given table. After discussing some options -- returning them as a delimited string, array, etc., we agreed that returning the columns in a table is the best option. &lt;/p&gt;
&lt;p&gt;This User Defined Table Function (UDTF) returns the columns for a table&#39;s primary key. The UDTF will return a table with a single column, each row in the table is one of the columns in the input table&#39;s primary key. If there is no primary key, the table will have no rows. For a single-column primary key, the table will have the one row, and for composite primary keys it will return all columns in the key.&lt;/p&gt;
&lt;p&gt;One thing you may notice is that the input to the UDTF is the table&#39;s DDL, not just the table&#39;s name. The reason for this is because UDTFs cannot execute SQL. The simplest way to handle this situation is to nest the GET_DDL function as the parameter for the GET_PK_COLUMNS function. You can see how this works in the code samp&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;create database DB_Primary_Keys;

/**************************************************************************************************************
*                                                                                                             *
*  Set up test tables with four types of primary key: Named composite, unnamed composite, inline, and none.   * 
*                                                                                                             *
**************************************************************************************************************/
 
-- Named multi-column PK
create or replace temporary table table1
(
column_name1 number NOT NULL,
column_name2 number NOT NULL,
column_name3 string,
CONSTRAINT Constraint_name PRIMARY KEY (column_name1, column_name2)
);
 
-- Unnamed multi-column PK
create temporary table table2
(
column_name1 number NOT NULL,
column_name2 number NOT NULL,
column_name3 string,
PRIMARY KEY (column_name1, column_name2)
);
 
-- Inline single-column PK
create or replace temporary table table3
(
column_name1 number primary key,
column_name2 number NOT NULL,
column_name3 string
);
 
-- No PK defined
create or replace temporary table table4
(
column_name1 number,
column_name2 number,
column_name3 string
);
 
/********************************************************************************************************
*                                                                                                       *
* User defined table function (UDTF) to get primary keys for a table.                                   *
*                                                                                                       *
* @param  {string}:  TABLE_DDL    The DDL for the table to get the PKs. Usually use get_ddl.            *
* @return {table}:                A table with the columns comprising the table&#39;s primary key           *
*                                                                                                       *
********************************************************************************************************/
create or replace function GET_PK_COLUMNS(TABLE_DDL string)
returns table (PK_COLUMN string)
language javascript
as
$$
{
    processRow: function get_params(row, rowWriter, context){
        var pkCols = getPKs(row.TABLE_DDL);
        for (i = 0; i &amp;lt; pkCols.length; i++) {
            rowWriter.writeRow({PK_COLUMN: pkCols[i]}); 
        }
         
        function getPKs(tableDDL) {
            var c;
            var keyword = &quot;primary key&quot;;
            var ins = -1;
            var s = tableDDL.split(&quot;&#92;n&quot;);
            for (var i = 0; i &amp;lt; s.length; i++) {  
                ins = s[i].indexOf(keyword);
                if (ins != -1) {
                    var colList = s[i].substring(ins + keyword.length);
                    colList = colList.replace(&quot;(&quot;, &quot;&quot;);
                    colList = colList.replace(&quot;)&quot;, &quot;&quot;);
                    var colArray = colList.split(&quot;,&quot;);
                    for (pkc = 0; c &amp;lt; colArray.length; pkc++) {
                        colArray[pkc] = colArray[pkc].trim();
                    }
                    return colArray;
                }
            }
            return [];  // No PK
        }
    }
}
$$;
 
/**************************************************************************************************************
*                                                                                                             *
*  Test execution of the UDTF.                                                                                * 
*                                                                                                             *
**************************************************************************************************************/
 
select * from table(get_pk_columns(get_ddl(&#39;table&#39;, &#39;table1&#39;))) PKS;  -- Multi-column PK with named constraint
select * from table(get_pk_columns(get_ddl(&#39;table&#39;, &#39;table2&#39;))) PKS;  -- Multi-column PK with no name for constraint
select * from table(get_pk_columns(get_ddl(&#39;table&#39;, &#39;table3&#39;))) PKS;  -- Single column PK inline definition
select * from table(get_pk_columns(get_ddl(&#39;table&#39;, &#39;table4&#39;))) PKS;  -- No PKs&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Oracle to Snowflake Table DDL Conversion</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/04/28/oracle-to-snowflake-table-ddl-conversion/"/>
    <updated>2020-04-28T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/04/28/oracle-to-snowflake-table-ddl-conversion/</id>
    <content type="html">&lt;p&gt;Jason Trewin at &lt;a rel=&quot;noreferrer noopener&quot; href=&quot;https://www.freshgravity.com/&quot; target=&quot;_blank&quot;&gt;FreshGravity&lt;/a&gt; provided this Oracle to Snowflake Table DDL conversion script. FreshGravity is a great Snowflake partner, and Jason is working on his second Snowflake deployment for our shared customers.&lt;/p&gt;
&lt;p&gt;He shared a great approach to DDL conversion from Oracle to Snowflake. I thought it was so useful that after some discussion he agreed to let me write up and post on it. &lt;/p&gt;
&lt;p&gt;You can modify the where clauses to select the tables you need to convert, and modify the mappings from Oracle table owners to Snowflake owners.&lt;/p&gt;
&lt;p&gt;Thanks and credit to Jason Trewin for contributing this useful script to the Snowflake community.&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;-- Script contributed to the Snowflake community courtesy of Jason Trewin at FreshGravity.

SELECT &#39;CREATE OR REPLACE TABLE &#39;  
        || NEW_SCHEMA_NAME
        || &#39;.&#39; ||  TABLE_NAME || &#39; (&#39; || LISTAGG(COLUMN_NAME || &#39; &#39; || CONVERTED_DATA_TYPE || CASE WHEN NULLABLE = &#39;N&#39; THEN &#39; NOT NULL&#39; ELSE &#39;&#39;END , &#39; , &#39;) WITHIN GROUP (ORDER BY COLUMN_ID)  || &#39;);&#39; --AS COLUMN_LIST
   FROM (      
 SELECT DATA_TYPE, COLUMN_NAME, ALL_TABLES.TABLE_NAME, ALL_TABLES.OWNER,DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE,
        CASE WHEN INSTR(DATA_TYPE,&#39;WITH LOCAL TIME ZONE&#39;) &gt; 0  THEN &#39;TIMESTAMP_TZ&#39;
             WHEN INSTR(DATA_TYPE,&#39;TIMESTAMP&#39;) &gt; 0 THEN &#39;TIMESTAMP_NTZ&#39;
             WHEN DATA_TYPE = &#39;DATE&#39; THEN &#39;DATE&#39;
             WHEN DATA_TYPE = &#39;XMLTYPE&#39; THEN &#39;VARIANT&#39;
             WHEN DATA_TYPE IN (&#39;BINARY_DOUBLE&#39;,&#39;NUMBER&#39;) THEN &#39;NUMBER&#39;||
                  CASE WHEN DATA_PRECISION IS NOT NULL THEN  &#39;(&#39;|| DATA_PRECISION || &#39;,&#39; || NVL(DATA_SCALE,0) || &#39;)&#39;
                            ELSE &#39;&#39; END
             WHEN DATA_TYPE IN (&#39;NVARCHAR2&#39;,&#39;VARCHAR2&#39;,&#39;VARCHAR&#39;,&#39;NVARCHAR&#39;) THEN &#39;TEXT(&#39; || DATA_LENGTH  || &#39;)&#39;
             WHEN DATA_TYPE IN (&#39;NCHAR&#39;,&#39;CHAR&#39;) THEN &#39;TEXT(1)&#39;
             WHEN DATA_TYPE = &#39;CLOB&#39; THEN &#39;TEXT(16777216)&#39;
             ELSE NULL END AS CONVERTED_DATA_TYPE
             ,CASE WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_1&#39; THEN &#39;SNOWFLAKE_OWNER_1&#39;  -- Map Oracle owners to Snowflake owners.
                   WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_2&#39; THEN &#39;SNOWFLAKE_OWNER_2&#39;
                   WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_3&#39; THEN &#39;SNOWFLAKE_OWNER_3&#39;                                      
                   WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_4&#39; THEN &#39;SNOWFLAKE_OWNER_4&#39;
				   WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_5&#39; THEN &#39;SNOWFLAKE_OWNER_5&#39;  
                   WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_6&#39; THEN &#39;SNOWFLAKE_OWNER_6&#39;                                      
               ELSE NULL END AS NEW_SCHEMA_NAME
             ,COLUMN_ID
  FROM ALL_TAB_COLUMNS
       INNER JOIN ALL_TABLES   
          ON ALL_TABLES.TABLE_NAME  =  ALL_TAB_COLUMNS.TABLE_NAME
         AND ALL_TABLES.OWNER       =  ALL_TAB_COLUMNS.OWNER
WHERE	-- Insert a where clause to decide which tables to include or exclude. This is just part of one approach: 
	ALL_TABLES.TABLE_NAME NOT IN (&#39;TABLE1&#39;,&#39;TABLE2&#39;,&#39;TABLE3&#39;)
  )
GROUP BY NEW_SCHEMA_NAME, TABLE_NAME

SELECT TEXT_LINE FROM (
SELECT 1 AS INDEXING_VALUE,
     TABLE_NAME ,
     OWNER,
        &#39;CREATE OR REPLACE TABLE &#39;  
        || CASE WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_1&#39; THEN &#39;SNOWFLAKE_OWNER_1&#39;  		-- Map Oracle table ownwers to Snowflake table owners
                WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_2&#39; THEN &#39;SNOWFLAKE_OWNER_2&#39;
                WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_3&#39; THEN &#39;SNOWFLAKE_OWNER_3&#39;                                      
                WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_4&#39; THEN &#39;SNOWFLAKE_OWNER_4&#39;
                WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_5&#39; THEN &#39;SNOWFLAKE_OWNER_5&#39;  
                WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_6&#39; THEN &#39;SNOWFLAKE_OWNER_6&#39;                                      
           ELSE NULL END         || &#39;.&#39; ||  TABLE_NAME || &#39;(&#39; AS TEXT_LINE, 1 AS COLUMN_ID
  FROM ALL_TABLES   
WHERE ALL_TABLES.OWNER LIKE &#39;ORACLE_OWNER_%&#39;	-- Set where condition for tables to process.
AND ALL_TABLES.TABLE_NAME IN (&#39;TABLE1&#39;, &#39;TABLE2&#39;, &#39;TABLE3&#39;)
UNION ALL
SELECT 2  AS INDEXING_VALUE, ALL_TABLES.TABLE_NAME , ALL_TABLES.OWNER,
        CASE WHEN COLUMN_ID = 1 THEN &#39;&#39; ELSE &#39;,&#39; END || COLUMN_NAME || &#39; &#39; ||
            CASE WHEN INSTR(DATA_TYPE,&#39;WITH LOCAL TIME ZONE&#39;) &gt; 0  THEN &#39;TIMESTAMP_TZ&#39;
             WHEN INSTR(DATA_TYPE,&#39;TIMESTAMP&#39;) &gt; 0 THEN &#39;TIMESTAMP_NTZ&#39;
             WHEN DATA_TYPE = &#39;DATE&#39; THEN &#39;DATE&#39;
             WHEN DATA_TYPE = &#39;XMLTYPE&#39; THEN &#39;VARIANT&#39;
             WHEN DATA_TYPE IN (&#39;BINARY_DOUBLE&#39;,&#39;NUMBER&#39;) THEN &#39;NUMBER&#39;||
                  CASE WHEN DATA_PRECISION IS NOT NULL THEN  &#39;(&#39;|| DATA_PRECISION || &#39;,&#39; || NVL(DATA_SCALE,0) || &#39;)&#39;
                            ELSE &#39;&#39; END
             WHEN DATA_TYPE IN (&#39;NVARCHAR2&#39;,&#39;VARCHAR2&#39;,&#39;VARCHAR&#39;,&#39;NVARCHAR&#39;) THEN &#39;TEXT(&#39; || DATA_LENGTH  || &#39;)&#39;
             WHEN DATA_TYPE IN (&#39;NCHAR&#39;,&#39;CHAR&#39;) THEN &#39;TEXT(1)&#39;
             WHEN DATA_TYPE = &#39;CLOB&#39; THEN &#39;TEXT(16777216)&#39;
             ELSE NULL END ||  CASE WHEN NULLABLE = &#39;N&#39; THEN &#39; NOT NULL&#39; ELSE &#39;&#39;END  AS CONVERTED_DATA_TYPE
           ,COLUMN_ID
  FROM ALL_TAB_COLUMNS
       INNER JOIN ALL_TABLES   
          ON ALL_TABLES.TABLE_NAME  =  ALL_TAB_COLUMNS.TABLE_NAME
         AND ALL_TABLES.OWNER       =  ALL_TAB_COLUMNS.OWNER
WHERE ALL_TABLES.OWNER LIKE &#39;ODS_%&#39;
AND ALL_TABLES.TABLE_NAME IN (&#39;TABLE1&#39;, &#39;TABLE2&#39;, &#39;TABLE3&#39;) 
 UNION ALL SELECT 3 AS INDEXING_VALUE,
     TABLE_NAME , OWNER,
        &#39;);&#39;, 1 AS COLUMN_ID
  FROM ALL_TABLES   
WHERE ALL_TABLES.OWNER LIKE &#39;ORACLE_OWNER_%&#39;
AND ALL_TABLES.TABLE_NAME IN (&#39;TABLE1&#39;, &#39;TABLE2&#39;, &#39;TABLE3&#39;)
)
ORDER BY TABLE_NAME, OWNER, INDEXING_VALUE, COLUMN_ID

SELECT  &#39;ALTER TABLE &#39;
        || CASE WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_1&#39; THEN &#39;SNOWFLAKE_OWNER_1&#39;  		-- Map Oracle table ownwers to Snowflake table owners
                WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_2&#39; THEN &#39;SNOWFLAKE_OWNER_2&#39;
                WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_3&#39; THEN &#39;SNOWFLAKE_OWNER_3&#39;                                      
                WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_4&#39; THEN &#39;SNOWFLAKE_OWNER_4&#39;
                WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_5&#39; THEN &#39;SNOWFLAKE_OWNER_5&#39;  
                WHEN ALL_TABLES.OWNER = &#39;ORACLE_OWNER_6&#39; THEN &#39;SNOWFLAKE_OWNER_6&#39;                                       
           ELSE NULL END         || &#39;.&#39; ||  TABLE_NAME
        || &#39; ADD PRIMARY KEY (&#39; ||PK_COLUMMN_LIST || &#39;);&#39;
FROM 
 (
SELECT --&#39;ALTER TABLE &#39; 
       COLS.OWNER, COLS.TABLE_NAME, LISTAGG(cols.COLUMN_NAME , &#39;,&#39;) WITHIN GROUP (ORDER BY POSITION) AS PK_COLUMMN_LIST
--       cols.table_name, cols.column_name, cols.POSITION, cons.owner
FROM all_constraints cons, all_cons_columns cols
WHERE cols.OWNER LIKE &#39;ORACLE_OWNER_%&#39;
AND cons.constraint_type = &#39;P&#39;
AND cons.constraint_name = cols.constraint_name
AND cons.owner = cols.owner
GROUP BY  COLS.OWNER, COLS.TABLE_NAME
)

&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Overloading JavaScript UDFs in Snowflake</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/04/11/overloading-javascript-udfs-in-snowflake/"/>
    <updated>2020-04-11T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/04/11/overloading-javascript-udfs-in-snowflake/</id>
    <content type="html">&lt;div class=&quot;wp-block-image&quot;&gt;&lt;figure class=&quot;aligncenter size-large is-resized&quot;&gt;&lt;img src=&quot;https://snowflake.pavlik.us/wp-content/uploads/2020/04/boxes-2624231_640.jpg&quot; alt=&quot;&quot; class=&quot;wp-image-369&quot; width=&quot;367&quot; height=&quot;377&quot; /&gt;&lt;figcaption&gt;A Base Function with Two Overloads&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Snowflake supports &lt;a rel=&quot;noreferrer noopener&quot; aria-label=&quot; (opens in a new tab)&quot; href=&quot;https://docs.snowflake.com/en/sql-reference/udf-overview.html#overloading-of-udf-names&quot; target=&quot;_blank&quot;&gt;overloading user defined functions&lt;/a&gt;. It&#39;s a great way to handle function calls with parameters of different data types or different numbers of parameters. Developers often overload functions to let users send only relevant parameters.&lt;/p&gt;
&lt;p&gt;Consider the common &lt;a rel=&quot;noreferrer noopener&quot; aria-label=&quot;SUBSTRING  (opens in a new tab)&quot; href=&quot;https://docs.snowflake.com/en/sql-reference/functions/substr.html#syntax&quot; target=&quot;_blank&quot;&gt;SUBSTRING &lt;/a&gt;function. You can call it using one of two overloads:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;select substr(&#39;Hello, world.&#39;, 8);     --This returns &quot;world.&quot;

select substr(&#39;Hello, world.&#39;, 8, 5);  --This returns &quot;world&quot;&lt;/pre&gt;
&lt;p&gt;In the first statement, the caller sent the string to use and the start position. Omitting the final parameter uses the overload with default behavior, returning to the end of the string.&lt;/p&gt;
&lt;p&gt;In the second statement, the caller decided to get rid of the final period. Adding the third parameter for length used the other overload to return five characters instead of the default behavior. &lt;/p&gt;
&lt;p&gt;This is a common design with overloaded functions. Mandatory parameters go on the left and optional parameters follow. Each allowable combination of parameters becomes an overload of the function. In this design, developers typically write one base function with all parameters. For the overloads with missing parameters, they&#39;ll call the base function using a default value for the missing parameter(s).&lt;/p&gt;
&lt;p&gt;This design ensures that there&#39;s only one place to maintain and debug code. The problem is Snowflake JavaScript UDFs cannot call other UDFs. While one way to deal with this is to write the same code in all overloads, it means three places to maintain, improve, and debug code. Fortunately, there&#39;s a way to write once base function and call it from overloaded functions using defaults.&lt;/p&gt;
&lt;p&gt;The solution is to write the base UDF with all parameters in JavaScript. For the overloads that simply call the base function using defaults for missing parameters, call the base JavaScript UDF using an overloaded SQL UDF. This works because SQL UDFs can call other UDFs, which JavaScript UDFs cannot do.&lt;/p&gt;
&lt;p&gt;In this example, the JavaScript UDF returns a column displaying a graphical progress bar. The opens are typical for progress bars: percent completion, number of decimal places to display on the percentage, and number of segments to display.&lt;/p&gt;
&lt;p&gt;The only one that can&#39;t be defaulted is the percent complete. It&#39;s okay to default to two decimal points and ten segments. &lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;-- Make a progress bar function that looks like this:   —————————— 24.53%

-- This is the main JavaScript function with all parameters.
create or replace function PROGRESS_BAR(PERCENTAGE float, DECIMALS float, SEGMENTS float)
returns string
language javascript
as
$$

    var percent = PERCENTAGE;
    
    if (isNaN(percent)) percent =   0;
    if (percent &amp;lt; 0)    percent =   0;
    if (percent &gt; 100)  percent = 100;

    percent        = percent.toFixed(DECIMALS);

    var filledSegments = Math.round(SEGMENTS * (percent / 100));
    var emptySegments  = SEGMENTS - filledSegments;

    var bar = &#39;—&#39;.repeat(filledSegments) + &#39;—&#39;.repeat(emptySegments);
 
    return bar + &quot; &quot; + percent + &quot;%&quot;;

$$;

-- This is an overload with only the percentage, using defaults for 
-- number of segments and decimal points to display on percentage.
create or replace function PROGRESS_BAR(PERCENTAGE float)
returns string
language sql
as
$$
    select progress_bar(PERCENTAGE, 2, 10)
$$;

-- This is an overload with the percentage and the option set for the
-- number of decimals to display. It uses a default for number of segments.
create or replace function PROGRESS_BAR(PERCENTAGE float, DECIMALS float)
returns string
language sql
as
$$
    select progress_bar(PERCENTAGE, DECIMALS, 10)
$$;

-- Call the main JavaScript function by sending all three parameters:
select progress_bar(24.5293, 0, 100) as PROGRESS;

-- Call the overloaded SQL function by omitting the number of segments (segments defaults to 10):
select progress_bar(24.5293, 1) as PROGRESS;

-- Call the overloaded SQL function specifying only the percentage 
-- (segments defaults to 10 and decimals to 2)
-- It should display like this:   —————————— 24.53%
select progress_bar(24.5293) as PROGRESS;&lt;/pre&gt;
&lt;p&gt;By the way, this UDF progress bar is fully functional. If you have a long-running process such as loading a large number of files, you can use it to monitor progress by refreshing the query periodically. Here&#39;s an example using a long progress bar and 24.41%:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;select progress_bar(24.41, 2, 100) as PROGRESS;&lt;/pre&gt;
&lt;figure class=&quot;wp-block-image size-large&quot;&gt;&lt;img src=&quot;https://snowflake.pavlik.us/wp-content/uploads/2020/04/ProgressBar-1024x168.png&quot; alt=&quot;&quot; class=&quot;wp-image-377&quot; /&gt;&lt;figcaption&gt;Progress Bar from Overloaded Snowflake UDF&lt;/figcaption&gt;&lt;/figure&gt;
</content>
  </entry>
  <entry>
    <title>Multi-Table Inserts with Good and Bad Row Tables</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/03/18/using-snowflake-multi-table-inserts-to-insert-good-rows-and-put-bad-ones-in-another-table/"/>
    <updated>2020-03-18T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/03/18/using-snowflake-multi-table-inserts-to-insert-good-rows-and-put-bad-ones-in-another-table/</id>
    <content type="html">&lt;div class=&quot;wp-block-image&quot;&gt;&lt;figure class=&quot;aligncenter size-large is-resized&quot;&gt;&lt;img src=&quot;https://snowflake.pavlik.us/wp-content/uploads/2020/03/QualityChecks.png&quot; alt=&quot;&quot; class=&quot;wp-image-340&quot; width=&quot;348&quot; height=&quot;278&quot; /&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Many customers have asked me how to separate good rows from bad rows during a data load. You can use the &lt;a rel=&quot;noreferrer noopener&quot; aria-label=&quot;VALIDATE  (opens in a new tab)&quot; href=&quot;https://docs.snowflake.net/manuals/sql-reference/functions/validate.html&quot; target=&quot;_blank&quot;&gt;validate&lt;/a&gt; table function to return all the errors encountered during the load. This may not be exactly what you need though. &lt;/p&gt;
&lt;p&gt;What you may be looking for is a design pattern like this:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Load data from files into a raw table&lt;/li&gt;&lt;li&gt;Except for major errors, insert rows with minor data quality issues into the raw table&lt;/li&gt;&lt;li&gt;After loading the raw table, insert good rows to staging (if more processing to do) or production&lt;/li&gt;&lt;li&gt;At the same time, insert bad rows into a separate table for examination of data quality problems&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;You usually load Snowflake tables from files. Files are string data, so you can define all columns in your raw table as string type. This ensures simple errors will not disrupt the load process. Major errors such as an improper number of columns in a row will generate an error during the load. You can specify the appropriate &lt;a rel=&quot;noreferrer noopener&quot; aria-label=&quot;copy option (opens in a new tab)&quot; href=&quot;https://docs.snowflake.net/manuals/sql-reference/sql/copy-into-table.html#copy-options-copyoptions&quot; target=&quot;_blank&quot;&gt;copy option&lt;/a&gt; to set how you want Snowflake to handle major errors like this.&lt;/p&gt;
&lt;p&gt;After defining a raw table, you can create a staging table  or a production table. Either option uses proper data types instead of all strings. You&#39;ll insert new rows to the target table while sending bad ones to a table containing the original bad values. You can then examine the bad rows to see why they failed to convert to the proper data types. &lt;/p&gt;
&lt;p&gt;You can use the following SQL script as a template for how to use Snowflake multi-table Inserts to do this.&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;-- Create a staging table with all columns defined as strings.
-- This will hold all raw values from the load files.
create or replace table SALES_RAW
(                                       -- Actual Data Type
  SALE_TIMESTAMP            string,     -- timestamp
  ITEM_SKU                  string,     -- int
  PRICE                     string,     -- number(10,2)
  IS_TAXABLE                string,     -- boolean
  COMMENTS                  string      -- string
);

-- Create the production table with actual data types.
create or replace table SALES_STAGE
(
  SALE_TIMESTAMP            timestamp,
  ITEM_SKU                  int,
  PRICE                     number(10,2),
  IS_TAXABLE                boolean,
  COMMENTS                  string
);

-- Simulate adding some rows from a load file. Two rows are good.
-- Four rows generate errors when converting to the data types.
insert into SALES_RAW 
    (SALE_TIMESTAMP, ITEM_SKU, PRICE, IS_TAXABLE, COMMENTS) 
    values
    (&#39;2020-03-17 18:21:34&#39;, &#39;23289&#39;, &#39;3.42&#39;,   &#39;TRUE&#39;,  &#39;Good row.&#39;),
    (&#39;2020-17-03 18:21:56&#39;, &#39;91832&#39;, &#39;1.41&#39;,   &#39;FALSE&#39;, &#39;Bad row: SALE_TIMESTAMP has the month and day transposed.&#39;),
    (&#39;2020-03-17 18:22:03&#39;, &#39;7O242&#39;, &#39;2.99&#39;,   &#39;T&#39;,     &#39;Bad row: ITEM_SKU has a capital &quot;O&quot; instead of a zero.&#39;),
    (&#39;2020-03-17 18:22:10&#39;, &#39;53921&#39;, &#39;$6.25&#39;,  &#39;F&#39;,     &#39;Bad row: PRICE should not have a dollar sign.&#39;),
    (&#39;2020-03-17 18:22:17&#39;, &#39;90210&#39;, &#39;2.49&#39;,   &#39;Foo&#39;,   &#39;Bad row: IS_TAXABLE cannot be converted to true or false&#39;),
    (&#39;2020-03-17 18:22:24&#39;, &#39;80386&#39;, &#39;1.89&#39;,   &#39;1&#39;,     &#39;Good row.&#39;);

-- Make sure the rows inserted okay.
select * from SALES_RAW;

-- Create a table to hold the bad rows.
create or replace table SALES_BAD_ROWS like SALES_RAW;

-- Using multi-table inserts (https://docs.snowflake.net/manuals/sql-reference/sql/insert-multi-table.html)
-- Insert good rows into SALES_STAGE and bad rows into SALES_BAD_ROWS
insert  first
  when  SALE_TIMESTAMP_X is null and SALE_TIMESTAMP is not null or
        ITEM_SKU_X       is null and SALE_TIMESTAMP is not null or
        PRICE_X          is null and PRICE          is not null or
        IS_TAXABLE_X     is null and IS_TAXABLE     is not null
  then 
        into SALES_BAD_ROWS
            (SALE_TIMESTAMP, ITEM_SKU, PRICE, IS_TAXABLE, COMMENTS)
        values
            (SALE_TIMESTAMP, ITEM_SKU, PRICE, IS_TAXABLE, COMMENTS)  
  else 
        into SALES_STAGE 
            (SALE_TIMESTAMP, ITEM_SKU, PRICE, IS_TAXABLE, COMMENTS) 
         values 
            (SALE_TIMESTAMP_X, ITEM_SKU_X, PRICE_X, IS_TAXABLE_X, COMMENTS)
select  try_to_timestamp (SALE_TIMESTAMP)   as SALE_TIMESTAMP_X,
        try_to_number    (ITEM_SKU, 10, 0)  as ITEM_SKU_X,
        try_to_number    (PRICE, 10, 2)     as PRICE_X,
        try_to_boolean   (IS_TAXABLE)       as IS_TAXABLE_X,
                                               COMMENTS, 
                                               SALE_TIMESTAMP,
                                               ITEM_SKU,
                                               PRICE,
                                               IS_TAXABLE
from    SALES_RAW;

-- Examine the two good rows
select * from SALES_STAGE;

-- Examine the four bad rows
select * from SALES_BAD_ROWS;&lt;/pre&gt;
&lt;p&gt;If your incoming values have no nulls or a default value, you can eliminate the null check from the SQL. Now&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;  when  SALE_TIMESTAMP_X is null or
        ITEM_SKU_X       is null or
        PRICE_X          is null or
        IS_TAXABLE_X     is null &lt;/pre&gt;
&lt;p&gt;This works because if the original value isn&#39;t null, the only reason it would be null is type cast failure. There&#39;s one final note on this section of the SQL. Why doesn&#39;t the &quot;when&quot; section with several &quot;and&quot; and &quot;or&quot; operators need parenthesis?&lt;/p&gt;
&lt;p&gt;The short answer is that AND has a higher &lt;a rel=&quot;noreferrer noopener&quot; aria-label=&quot;operator precedence (opens in a new tab)&quot; href=&quot;https://en.wikipedia.org/wiki/Order_of_operations&quot; target=&quot;_blank&quot;&gt;operator precedence&lt;/a&gt; than the OR operator. This is true in SQL and most programming languages and seems familiar to many people. If it improves clarity you can add parenthesis. As an academic exercise, this shows the operator precedence of &quot;and&quot; and &quot;or&quot;.&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;select TRUE and TRUE or FALSE and FALSE; -- In operator precedence, AND comes first, then left to right. This evaluates to TRUE.

-- Processing AND first
select (TRUE and TRUE) or (FALSE and FALSE); -- This is functionally equivalent to the above statement.

-- Processing OR first
select TRUE and (TRUE or FALSE) and FALSE; -- This shows what would happen if OR had higher operator precedence

-- Processing only left to right
select ((TRUE and TRUE) or FALSE) and FALSE; -- This shows what would happen with no operator precedence, just left to right&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Helper Functions in Snowflake Stored Procedures</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/03/16/helper-functions-in-snowflake-stored-procedures/"/>
    <updated>2020-03-16T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/03/16/helper-functions-in-snowflake-stored-procedures/</id>
    <content type="html">&lt;div class=&quot;wp-block-image&quot;&gt;&lt;figure class=&quot;aligncenter size-large&quot;&gt;&lt;img src=&quot;https://snowflake.pavlik.us/wp-content/uploads/2020/03/Clippy_StoredProcedure.jpg&quot; alt=&quot;&quot; class=&quot;wp-image-316&quot; /&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Snowflake supports &lt;a rel=&quot;noreferrer noopener&quot; aria-label=&quot;stored procedures using JavaScript (opens in a new tab)&quot; href=&quot;https://docs.snowflake.net/manuals/sql-reference/stored-procedures.html&quot; target=&quot;_blank&quot;&gt;JavaScript stored procedures&lt;/a&gt;.  You may choose to start by copying and modifying a sample Snowflake stored procedure from the documentation, often &lt;a rel=&quot;noreferrer noopener&quot; aria-label=&quot;this one (opens in a new tab)&quot; href=&quot;https://docs.snowflake.net/manuals/sql-reference/stored-procedures-usage.html#retrieving-result-set-metadata&quot; target=&quot;_blank&quot;&gt;this one&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As you add more SQL statements, exception handling and increase code complexity, having all code in the main JavaScript function risks becoming spaghetti code. &lt;/p&gt;
&lt;p&gt;Fortunately, Snowflake stored procedures allow more than one function. In JavaScript, helper functions are additional functions called from a main function. &lt;/p&gt;
&lt;p&gt;It&#39;s easy to write a helper function. Just before the main function&#39;s final curly bracket, add the following:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;function HelperFunction(stringIn) {

    //Do something here, and then return the value:
    var s = stringIn;
    return s;
}&lt;/pre&gt;
&lt;p&gt;You can also use the &lt;a rel=&quot;noreferrer noopener&quot; aria-label=&quot;Snowflake Stored Procedure API (opens in a new tab)&quot; href=&quot;https://docs.snowflake.net/manuals/sql-reference/stored-procedures-api.html&quot; target=&quot;_blank&quot;&gt;Snowflake Stored Procedure API&lt;/a&gt; inside helper functions. Here two helper functions using the Snowflake SP API that make your main function more readable and modular.  ExecuteNonQuery executes a DML statement or SQL statement that does not return a table. ExecuteSingleValueQuery fetches the first row&#39;s value for a specified column. You can use this to retrieve flags and settings or other values from control tables.&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;create or replace procedure SampleSP()
returns string
language javascript
as
$$
    var s;

    try{
        ExecuteNonQuery(&quot;create or replace table MY_NATION_TABLE like SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.NATION;&quot;);
        ExecuteNonQuery(&quot;insert into MY_NATION_TABLE select * from SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.NATION;&quot;);
        s = ExecuteSingleValueQuery(&quot;N_NAME&quot;, &quot;select * from MY_NATION_TABLE where N_NATIONKEY = 24;&quot;);
        ExecuteNonQuery(&quot;drop table MY_NATION_TABLE;&quot;);
        return s;
    }
    catch(err){
        return err;
    }
// ----------------------------------------------------------------------------------
// Main function above; helper functions below

    function ExecuteNonQuery(queryString) {
        var out = &#39;&#39;;
        cmd1 = {sqlText: queryString};
        stmt = snowflake.createStatement(cmd1);
        var rs;
        try{
            rs = stmt.execute();
            rs.next();
            out = &quot;SUCCESS: &quot; + rs.getColumnValue(1);
        }
        catch(err) {
            throw &quot;ERROR: &quot; + err.message.replace(/&#92;n/g, &quot; &quot;);
        }
        return out;
    }

    function ExecuteSingleValueQuery(columnName, queryString) {
        var out;
        cmd1 = {sqlText: queryString};
        stmt = snowflake.createStatement(cmd1);
        var rs;
        try{
            rs = stmt.execute();
            rs.next();
            return rs.getColumnValue(columnName);
        }
        catch(err) {
            if (err.message.substring(0, 18) == &quot;ResultSet is empty&quot;){
                throw &quot;ERROR: No rows returned in query.&quot;;
            } else {
                throw &quot;ERROR: &quot; + err.message.replace(/&#92;n/g, &quot; &quot;);
            } 
        }
        return out;
    }
$$;

call SampleSP();&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Getting a Complete List of User Privileges in Snowflake</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/02/21/getting-a-complete-list-of-user-privileges-in-snowflake/"/>
    <updated>2020-02-21T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/02/21/getting-a-complete-list-of-user-privileges-in-snowflake/</id>
    <content type="html">&lt;p&gt;These queries don&#39;t need much explanation. I&#39;ve had some customers request how to get a complete list of user privileges, often for auditing purposes. The two queries below will show the role hierarchy (which roles have been granted which other roles) and a complete list of effective permissions for each user.&lt;/p&gt;
&lt;p&gt;For instance, if someone grants user &#39;MARY&#39; the &#39;PLAN_9&#39; role, and that role has a privilege to select from &#39;TABLE_X&quot;, then one row in the result will show that MARY can select from TABLE_X because she&#39;s been granted the PLAN_9 role. All other users in the PLAN_9 role will also show a row with this set of user, role granting the privilege, and then the privilege itself.&lt;/p&gt;
&lt;p&gt;Snowflake enforces a best practice for security and governance called RBAC, role based access control. Privileges go to roles, not directly to users. To grant a user a privilege, add the user to a role with the privilege.&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;-- The data returned by both queries is in the
-- SNOWFLAKE database, which has latency of up
-- to 3 hours to reflect changes

-- Get the effective role hierarchy for each user.
with
   -- CTE gets all the roles each role is granted
   ROLE_MEMBERSHIPS(ROLE_GRANTEE, ROLE_GRANTED_THROUGH_ROLE)
   as
    (
    select   GRANTEE_NAME, &quot;NAME&quot;
    from     SNOWFLAKE.ACCOUNT_USAGE.GRANTS_TO_ROLES
    where    GRANTED_TO = &#39;ROLE&#39; and
             GRANTED_ON = &#39;ROLE&#39; and
             DELETED_ON is null
    ),
    -- CTE gets all roles a user is granted
    USER_MEMBERSHIPS(ROLE_GRANTED_TO_USER, USER_GRANTEE, GRANTED_BY)
    as
     (
     select ROLE,
            GRANTEE_NAME,
            GRANTED_BY
     from SNOWFLAKE.ACCOUNT_USAGE.GRANTS_TO_USERS
     where DELETED_ON is null
     )
-- 
select 
        USER_GRANTEE,
        case
            when ROLE_GRANTED_THROUGH_ROLE is null 
                then ROLE_GRANTED_TO_USER 
            else ROLE_GRANTED_THROUGH_ROLE
        end 
        EFFECTIVE_ROLE,
        GRANTED_BY,
        ROLE_GRANTEE,
        ROLE_GRANTED_TO_USER,
        ROLE_GRANTED_THROUGH_ROLE
from    USER_MEMBERSHIPS U
    left join ROLE_MEMBERSHIPS R
        on U.ROLE_GRANTED_TO_USER = R.ROLE_GRANTEE
;

--------------------------------------------------------------------------------------------------

-- This gets all the grants for all of the users:
with 
    ROLE_MEMBERSHIPS
        (
            ROLE_GRANTEE, 
            ROLE_GRANTED_THROUGH_ROLE
        )
    as
    (
        -- This lists all the roles a role is in
        select   GRANTEE_NAME, &quot;NAME&quot;
        from     SNOWFLAKE.ACCOUNT_USAGE.GRANTS_TO_ROLES
        where    GRANTED_TO = &#39;ROLE&#39; and
                 GRANTED_ON = &#39;ROLE&#39; and
                 DELETED_ON is null
    ),
    USER_MEMBERSHIPS
        (
            ROLE_GRANTED_TO_USER,
            USER_GRANTEE,
            GRANTED_BY
        )
    as
     (
        select ROLE,GRANTEE_NAME,GRANTED_BY
        from SNOWFLAKE.ACCOUNT_USAGE.GRANTS_TO_USERS
        where DELETED_ON is null
     ),
    EFFECTIVE_ROLES
    (
        USER_GRANTEE,
        EFFECTIVE_ROLE,
        GRANTED_BY,
        ROLE_GRANTEE,
        ROLE_GRANTED_TO_USER,
        ROLE_GRANTED_THROUGH_ROLE
    )
    as
    (
        select 
            USER_GRANTEE,
            case 
                when ROLE_GRANTED_THROUGH_ROLE is null
                    then ROLE_GRANTED_TO_USER
                else ROLE_GRANTED_THROUGH_ROLE
            end
            EFFECTIVE_ROLE,
            GRANTED_BY,
            ROLE_GRANTEE,
            ROLE_GRANTED_TO_USER,
            ROLE_GRANTED_THROUGH_ROLE
        from USER_MEMBERSHIPS U
            left join ROLE_MEMBERSHIPS R
            on U.ROLE_GRANTED_TO_USER = R.ROLE_GRANTEE
    ),
    GRANT_LIST
        (
            CREATED_ON,
            MODIFIED_ON,
            PRIVILEGE,
            GRANTED_ON, 
            &quot;NAME&quot;,
            TABLE_CATALOG,
            TABLE_SCHEMA,
            GRANTED_TO,
            GRANTEE_NAME,
            GRANT_OPTION
        )
    as
    (
        -- This shows all the grants (other than to roles)
        select  CREATED_ON,
                MODIFIED_ON,
                PRIVILEGE,
                &quot;NAME&quot;,
                TABLE_CATALOG,
                TABLE_SCHEMA,
                GRANTED_TO,
                GRANTEE_NAME,
                GRANT_OPTION,
                GRANTED_ON
        from    SNOWFLAKE.ACCOUNT_USAGE.GRANTS_TO_ROLES
        where   GRANTED_ON &amp;lt;&gt; &#39;ROLE&#39; and
                PRIVILEGE &amp;lt;&gt; &#39;USAGE&#39; and 
                DELETED_ON is null
    )
select * from EFFECTIVE_ROLES R
    left join GRANT_LIST G 
        on G.GRANTED_TO = R.EFFECTIVE_ROLE
where G.PRIVILEGE is not null
;&lt;/pre&gt;
&lt;p&gt; &lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Executing Multiple SQL Statements in a Stored Procedure - Part Deux</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/02/01/executing-multiple-sql-statements-in-a-stored-procedure-part-deux/"/>
    <updated>2020-02-01T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/02/01/executing-multiple-sql-statements-in-a-stored-procedure-part-deux/</id>
    <content type="html">&lt;p&gt;A customer requested the ability to &lt;a href=&quot;https://snowflake.pavlik.us/index.php/2019/08/22/executing-multiple-sql-statements-in-a-stored-procedure/&quot;&gt;execute multiple SQL statements that result from a query&lt;/a&gt;. Today I learned about a new use case that required some augmentation of the stored procedure. Specifically, what happens when one of the many generated SQL statements encounters an error?&lt;/p&gt;
&lt;p&gt;This updated stored procedure handles generated SQL statements that may encounter an error. You have two options to handle errors -- report errors and continue or report first error and stop.&lt;/p&gt;
&lt;p&gt;The stored procedure is overloaded, meaning that you can call it with or without the second parameter &quot;continueOnError&quot;. If you do not supply the second parameter, it will default to false and stop after the first error.&lt;/p&gt;
&lt;p&gt;The output of the stored procedure is as follows:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;&amp;lt;SQL statement to run&gt; --Succeeded
&amp;lt;SQL statement to run&gt; --Failed: &amp;lt;reason for failure&gt;&lt;/pre&gt;
&lt;p&gt;By indicating the success or failure status as a SQL comment, you can modify and re-run the line manually or do some troubleshooting.&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;use database TEST;
use warehouse TEST;
 
create or replace procedure RunBatchSQL(sqlCommand String)
    returns string
    language JavaScript
as
$$
/**
 * Stored procedure to execute multiple SQL statements generated from a SQL query
 * Note that this procedure will always use the column named &quot;SQL_COMMAND&quot;
 * This overload of the function will stop after encountering the first error.
 *
 * @param {String} sqlCommand: The SQL query to run to generate one or more SQL commands 
 * @return {String}: A string containing all the SQL commands executed, each separated by a newline. 
 */
      cmd1_dict = {sqlText: SQLCOMMAND};
      stmt = snowflake.createStatement(cmd1_dict);
      rs = stmt.execute();
      var s = &#39;&#39;;
      var CONTINUEONERROR = false; // Default to false for overloaded function
      while (rs.next()) {
          try{
                cmd2_dict = {sqlText: rs.getColumnValue(&quot;SQL_COMMAND&quot;)};
                stmtEx = snowflake.createStatement(cmd2_dict);
                stmtEx.execute();
                s += rs.getColumnValue(1) + &quot; --Succeeded&quot; + &quot;&#92;n&quot;;
             }
          catch(err) {
                s += rs.getColumnValue(1) + &quot; --Failed: &quot; + err.message.replace(/&#92;n/g, &quot; &quot;) + &quot;&#92;n&quot;;
                if (!CONTINUEONERROR) return s;
          }
      }
      return s;
$$;
 
create or replace procedure RunBatchSQL(sqlCommand String, continueOnError Boolean)
    returns string
    language JavaScript
as
$$
/**
 * Stored procedure to execute multiple SQL statements generated from a SQL query
 * Note that this procedure will always use the column named &quot;SQL_COMMAND&quot;.
 * This overload of the function will continue on errors if &quot;continueOnError&quot; = true.
 *
 * @param {String} sqlCommand: The SQL query to run to generate one or more SQL commands 
 * @param {Boolean} continueOnError: If true, continues on error. If false, stops after first error.
 * @return {String}: A string containing all the SQL commands executed, each separated by a newline. 
 */
      cmd1_dict = {sqlText: SQLCOMMAND};
      stmt = snowflake.createStatement(cmd1_dict);
      rs = stmt.execute();
      var s = &#39;&#39;;
      while (rs.next()) {
          try{
                cmd2_dict = {sqlText: rs.getColumnValue(&quot;SQL_COMMAND&quot;)};
                stmtEx = snowflake.createStatement(cmd2_dict);
                stmtEx.execute();
                s += rs.getColumnValue(1) + &quot; --Succeeded&quot; + &quot;&#92;n&quot;;
             }
          catch(err) {
                s += rs.getColumnValue(1) + &quot; --Failed: &quot; + err.message.replace(/&#92;n/g, &quot; &quot;) + &quot;&#92;n&quot;;
                if (!CONTINUEONERROR) return s;
          }
      }
      return s;
$$
;

-- This is a select query that will generate a list of SQL commands to excute, in this case some grant statements. 
-- This SQL will generate rows to grant select on all tables for the DBA role (change to specify another role). 
select distinct (&#39;grant select on table &#39; || table_schema || &#39;.&#39; || table_name || &#39; to role DBA;&#39;) AS SQL_COMMAND
from INFORMATION_SCHEMA.TABLE_PRIVILEGES
where TABLE_SCHEMA &amp;lt;&gt; &#39;AUDIT&#39;
order by SQL_COMMAND;
 
-- As a convienience, this grabs the last SQL run so that it&#39;s easier to insert into the parameter used to call the stored procedure. 
set query_text = (  select QUERY_TEXT
                    from table(information_schema.query_history(result_limit =&gt; 2))
                    where SESSION_ID = Current_Session() and QUERY_TYPE = &#39;SELECT&#39; order by START_TIME desc);
 
-- Confirm that the query_text variable has the correct SQL query to generate our SQL commands (grants in this case) to run.
select $query_text as QUERY_TEXT;
 
-- Run the stored procedure. Note that to view its output better, double click on the output to see it in multi-line format,
Call RunBatchSQL($query_text, true);
 
--Check the last several queries run to make sure it worked.
select QUERY_TEXT
from table(information_schema.query_history(result_limit =&gt; 100))
where SESSION_ID = Current_Session() order by START_TIME desc;
&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Connecting Microsoft Access to Snowflake</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/01/17/connecting-microsoft-access-to-snowflake/"/>
    <updated>2020-01-17T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/01/17/connecting-microsoft-access-to-snowflake/</id>
    <content type="html">&lt;div class=&quot;wp-block-image&quot;&gt;&lt;figure class=&quot;aligncenter size-large is-resized&quot;&gt;&lt;img src=&quot;https://snowflake.pavlik.us/wp-content/uploads/2020/01/Microsoft_Access_2013_logo.svg_-1024x1005.png&quot; alt=&quot;&quot; class=&quot;wp-image-263&quot; width=&quot;263&quot; height=&quot;258&quot; /&gt;&lt;figcaption&gt;Microsoft Access&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;A customer sent me an interesting request today asking how to connect Microsoft Access to Snowflake. I connected Excel to Snowflake, but had never tried with Access. I thought that since they&#39;re both Microsoft Office products, it would work pretty much the same. That turned out not to be the case, at least at first.&lt;/p&gt;
&lt;p&gt;A bit of research indicated that Access doesn&#39;t ask for credentials; you have to store them in the Data Source Name (DSN). It&#39;s possible to use Visual Basic for Applications (VBA) to ask for credentials each use, but it introduces other issues. On the other side of the connection, Snowflake does not store credentials in the ODBC DSN. You have a standoff situation.&lt;/p&gt;
&lt;p&gt;Fortunately there&#39;s an easy and robust solution. &lt;a href=&quot;https://www.cdata.com/company/&quot;&gt;CData&lt;/a&gt;, a company specializing in data access and connectivity solutions, has a Snowflake ODBC driver. CData has a reputation for high-quality products. Developers often choose to use their ODBC or other connectors instead of the ones database companies provide free. The &lt;a href=&quot;https://www.cdata.com/drivers/snowflake&quot;&gt;CData ODBC driver for Snowflake&lt;/a&gt; comes with a 30-day free trial and is bi-directional. You can read from and write to Snowflake with it.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Connecting Access to Snowflake is easy. In the &quot;Url&quot; box, take your Snowflake URL and enter the complete URL up to and including &quot;snowflakecomputing.com&quot; and enter it. In the &quot;Account&quot; box, enter just the first part after https:// not including any indicator of location such as us-east1. For example:&lt;/p&gt;
&lt;figure class=&quot;wp-block-image size-large&quot;&gt;&lt;img src=&quot;https://snowflake.pavlik.us/wp-content/uploads/2020/01/CData.png&quot; alt=&quot;&quot; class=&quot;wp-image-266&quot; /&gt;&lt;figcaption&gt;CData ODBC DSN for Snowflake&lt;/figcaption&gt;&lt;/figure&gt;
</content>
  </entry>
  <entry>
    <title>Snowflake UDF to Get Payment Card Type</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/01/14/snowflake-udf-to-get-payment-card-type/"/>
    <updated>2020-01-14T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/01/14/snowflake-udf-to-get-payment-card-type/</id>
    <content type="html">&lt;div class=&quot;wp-block-image&quot;&gt;&lt;figure class=&quot;aligncenter size-large is-resized&quot;&gt;&lt;img src=&quot;https://snowflake.pavlik.us/wp-content/uploads/2020/01/credit_cards.jpg&quot; alt=&quot;Payment Cards&quot; class=&quot;wp-image-251&quot; width=&quot;420&quot; height=&quot;263&quot; /&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This User Defined Function (UDF) doesn&#39;t require much explanation. Payment card number goes in; payment card type comes out. Since it is designed for speed, it does not validate the check digit. A subsequent post will provide a UDF to validate the check digit using the Luhn algorithm.&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;/********************************************************************************************************************

Function:    PaymentCardType
Description: Decodes the type of payment card from Visa, Mastercard, AMEX, etc.
Parameters:  A string indicating the type of payment card, or a blank string if not identified. 

*********************************************************************************************************************/
create or replace function PaymentCardType(cardNumber string)
  returns string 
  language javascript
  strict
  as &#39;
     
    //Remove all spaces and dashes. Simply ignore them.
    NUMBER = CARDNUMBER.replace(/ /g, &quot;&quot;);
    NUMBER = NUMBER.replace(/-/g, &quot;&quot;);
     
     
    // Visa
    var re = new RegExp(&quot;(4[0-9]{15})&quot;);
    if (NUMBER.match(re) != null)
        return &quot;Visa&quot;;

    // Mastercard
    re = new RegExp(&quot;(5[1-5][0-9]{14})&quot;);
    if (NUMBER.match(re) != null)
        return &quot;Mastercard&quot;;

    // AMEX
    re = new RegExp(&quot;^3[47]&quot;);
    if (NUMBER.match(re) != null)
        return &quot;AMEX&quot;;

    // Discover
    re = new RegExp(&quot;^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)&quot;);
    if (NUMBER.match(re) != null)
        return &quot;Discover&quot;;

    // Diners
    re = new RegExp(&quot;^36&quot;);
    if (NUMBER.match(re) != null)
        return &quot;Diners&quot;;

    // Diners - Carte Blanche
    re = new RegExp(&quot;^30[0-5]&quot;);
    if (NUMBER.match(re) != null)
        return &quot;Diners - Carte Blanche&quot;;

    // JCB
    re = new RegExp(&quot;^35(2[89]|[3-8][0-9])&quot;);
    if (NUMBER.match(re) != null)
        return &quot;JCB&quot;;

    // Visa Electron
    re = new RegExp(&quot;^(4026|417500|4508|4844|491(3|7))&quot;);
    if (NUMBER.match(re) != null)
        return &quot;Visa Electron&quot;;

    return &quot;&quot;;
 
  &#39;;

-- Test the UDF:
select PaymentCardType(&#39;4470653497431234&#39;);&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Snowflake Relationships - Java Utilities</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/01/12/snowflake-relationships-java-utilities/"/>
    <updated>2020-01-12T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/01/12/snowflake-relationships-java-utilities/</id>
    <content type="html">&lt;div class=&quot;wp-block-image&quot;&gt;&lt;figure class=&quot;aligncenter size-large&quot;&gt;&lt;img src=&quot;https://cdn.pixabay.com/photo/2016/12/09/18/30/database-schema-1895779__480.png&quot; alt=&quot;&quot; /&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Administrators usually disable parent-child relational constraint enforcement, especially in OLAP databases. Snowflake allows definition of parent-child relationships, but currently does not enable enforcement. This approach enables documentation at the table and view level. It also allows integration with Entity Relationship Diagram (ERD) solutions or custom data dictionaries.&lt;/p&gt;
&lt;p&gt;Snowflake stores the relationship information in the table Data Definition Language (DDL) representation of each table or view. Since there appears to be no centralized location to read the relationships, I wrote a Java project to capture them automatically. &lt;/p&gt;
&lt;p&gt;In its present state it has some limitations. Chief among them is that I have tested it using only single-column primary and foreign keys. I think, though I have not yet confirmed, that it should work with multi-column keys. It could run into parsing issues due to differences in how Snowflake stores DDL lines for multi-column primary and foreign keys. This should be a simple problem to address, but I&#39;ve not yet tested it.&lt;/p&gt;
&lt;p&gt;The attached Java project, Snowflake_Utilities, has a class named SchemaInfo. The SchemaInfo class will collect more schema information in future updates. The initial preview focuses on collecting relationship information. It can:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Return a primary key for a table or view&lt;/li&gt;&lt;li&gt;Return all foreign keys defined for a table or view, along with the name and key on the parent&lt;/li&gt;&lt;li&gt;Get all primary keys for every table and view across an entire Snowflake account&lt;/li&gt;&lt;li&gt;Get all foreign keys for every table and view across an entire Snowflake account&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;The included Java source, exported from Eclipse, should be easy to configure. The main thing to add to the project build path is the latest Snowflake JDBC driver. &lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://snowflake.pavlik.us/wp-content/uploads/2020/01/SnowflakeConstraints.zip&quot;&gt;/wp-content/uploads/2020/01/SnowflakeConstraints.zip&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Snowflake Streams Made Simple</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/01/12/snowflake-streams-made-simple/"/>
    <updated>2020-01-12T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/01/12/snowflake-streams-made-simple/</id>
    <content type="html">&lt;div class=&quot;wp-block-image&quot;&gt;&lt;figure class=&quot;aligncenter size-large is-resized&quot;&gt;&lt;img src=&quot;https://snowflake.pavlik.us/wp-content/uploads/2020/01/stream.jpg&quot; alt=&quot;&quot; class=&quot;wp-image-241&quot; width=&quot;487&quot; height=&quot;274&quot; /&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;h4&gt;Snowflake streams demystified&lt;/h4&gt;
&lt;p&gt;The term stream has a lot of usages and meanings in information technology. This is one of the reasons the Snowflake stream feature has excited interest, but also raised confusion. Technologists often use the term stream interchangeably with a platform for handling a real-time data feed, such as Kafka. Snowflake streams are something different.&lt;/p&gt;
&lt;p&gt;Often distinctions in technology are subtle. Fortunately this isn&#39;t one of those times. Snowflake streams are nothing like Kafka, Spark Streaming or Flume. They capture change data, i.e., CDC and show the changes in a table. Also, Snowflake streams are not always &quot;streaming&quot;. They capture changes to a table whether they&#39;re happening in a stream, micro-batches, or batch processes. &lt;/p&gt;
&lt;h4&gt;Why use stream tables&lt;/h4&gt;
&lt;p&gt;There are lots of reasons. One of the most common is keeping a staging table and production table in sync. Before discussing how, let&#39;s discuss &lt;strong&gt;&lt;em&gt;why&lt;/em&gt;&lt;/strong&gt; you might want to have a staging table at all. Why not just process changes directly in the production table?&lt;/p&gt;
&lt;p&gt;The main reason is to protect the production table from bad changes. Perhaps a load terminated abnormally due to file corruption, or a delete zapped more rows than planned. It&#39;s better to back off those changes and clean up in a staging table than in a production table. Once you&#39;re satisfied that the changes are ready to promote to production (satisfied by automatic check or manual action), you can use the change data captured in the stream to synchronize the changes to the production table. &lt;/p&gt;
&lt;p&gt;Of course, that&#39;s only one reason to use a CDC stream. Since it&#39;s the most straightforward, let&#39;s start with that one. Snowflake streams provide a powerful way to deal with changing data sets. I&#39;ll discuss some very intriguing uses for streams in future posts.&lt;/p&gt;
&lt;h4&gt;Simplifying how to use streams&lt;/h4&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.snowflake.net/manuals/user-guide/streams.html&quot;&gt;documentation for streams&lt;/a&gt; is comprehensive. It covers a great deal of ground, attempting to show every capability and option. In contrast, this article presents a single simplified use case. Specifically, the common use case of pushing all changes to a staging table, and using a CDC stream to control changes to a corresponding production table. Hopefully this will allow the reader to learn from a simple example, tinker with it, and come up with your own uses.&lt;/p&gt;
&lt;p&gt;The SQL script walks through the key concepts of Snowflake streams. A subsequent post will elaborate on some key aspects of how and why streams work the way they do. The final two SQL statements merit some discussion. The next to last one shows that a single merge statement can perform 100% of the insert, delete, and update operations in the stream in a single step. Rather than expecting you to reverse-engineer why that is, there&#39;s an annotated SQL explaining how it works. The very last SQL statement is a handy (and in Snowflake very rapid) way to dump any differences between any number of columns in two tables. &lt;/p&gt;
&lt;figure class=&quot;wp-block-image size-large is-style-default&quot;&gt;&lt;a href=&quot;https://snowflake.pavlik.us/wp-content/uploads/2021/02/Snowflake_Streams.txt&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;img src=&quot;https://snowflake.pavlik.us/wp-content/uploads/2021/01/DownloadSQL.png&quot; alt=&quot;&quot; class=&quot;wp-image-479&quot; /&gt;&lt;/a&gt;&lt;/figure&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;-- Set the context. Be sure to use a test database and extra small 
-- test warehouse.
use warehouse TEST;
use database TEST;
use role SYSADMIN;

-- Create a schema to test streams
create or replace schema TEST_STREAMS;

-- Create a STAGING table to hold our changed data for our CDC pipeline
create or replace table STAGING 
  (ID int, CHANGE_NUMBER string, HOW_CHANGED string, FINAL_VALUE string);

-- Create the target PRODUCTION table with a schema identical to STAGING
create or replace table PRODUCTION like STAGING;

-- Create a stream on the STAGING table
create or replace stream STAGING_STREAM on table STAGING;

-- Examine the STAGING table... It&#39;s a simple, four-column table:
select * from STAGING;

-- Examine the STAGING_STREAM... It&#39;s got 3 new columns called
-- METADATA$ACTION, METADATA$ISUPDATE, and METADATA$ROW_ID
select * from STAGING_STREAM;

-- 1st change to STAGING table
-- Let&#39;s insert three rows into the STAGING table:
insert into STAGING (ID, CHANGE_NUMBER, HOW_CHANGED, FINAL_VALUE) values
    (1, &#39;1st change to STAGING table&#39;, &#39;Inserted&#39;, &#39;Original Row 1 Value&#39;),
    (2, &#39;1st change to STAGING table&#39;, &#39;Inserted&#39;, &#39;Original Row 2 Value&#39;),
    (3, &#39;1st change to STAGING table&#39;, &#39;Inserted&#39;, &#39;Original Row 3 Value&#39;);
    
-- Let&#39;s look at the STAGING table now to see our three rows:
select * from STAGING;

-- Now, let&#39;s look at our stream. Notice there are three &quot;INSERT&quot; metadata
-- actions and three FALSE for metadata &quot;ISUPDATE&quot;:
select * from STAGING_STREAM;

-- The documentation for streams discusses how DML operations will advance the 
-- position of the stream. Note that a SELECT is *not* a DML operation, and will
-- not advance the stream. Let&#39;s run the select again and see all three rows are
-- still there no matter how many times we SELECT from the stream:
select * from STAGING_STREAM;

-- Recall that a Snowflake stream indicates all the changes you need to make 
-- to keep a target table (PRODUCTION) in sync with the staging table where the
-- stream is tracking the changes (STAGING). With this preamble, can you guess
-- what will happen when you delete all rows in the staging table *before* 
-- you consume the stream?

-- 2st change to STAGING table
delete from STAGING;

-- Let&#39;s SELECT from STAGING_STREAM and see what&#39;s there:
select * from STAGING_STREAM;

-- There are no rows. Why is this? Why does the stream not show the three 
-- inserted rows and then the three deleted rows? Recall the underlying purpose
-- of Snowflake streams, to keep a staging and production table in sync. Since
-- we inserted and deleted the rows *before* we used (consumed) the stream in
-- a DML action, we didn&#39;t need to insert and delete the rows to sync the tables.

-- Now, let&#39;s reinsert the rows:

-- 3rd change to STAGING table
insert into STAGING (ID, CHANGE_NUMBER, HOW_CHANGED, FINAL_VALUE) values
  (1, &#39;3rd change to STAGING table&#39;, &#39;Inserted after deleted&#39;, &#39;Original Row 1 Value&#39;),
  (2, &#39;3rd change to STAGING table&#39;, &#39;Inserted after deleted&#39;, &#39;Original Row 2 Value&#39;),
  (3, &#39;3rd change to STAGING table&#39;, &#39;Inserted after deleted&#39;, &#39;Original Row 3 Value&#39;);
    
    
-- Now let&#39;s look at the stream again. We expect to see three inserts of the 
-- new change:
select * from STAGING_STREAM;

-- Okay, now let&#39;s show what happens when you use the stream as part of a DML
-- transaction, which is an INDERT, DELETE, UPDATE, or MERGE:
insert into PRODUCTION 
  select ID, CHANGE_NUMBER, HOW_CHANGED, FINAL_VALUE from STAGING_STREAM;

-- The rows are in PRODUCTION:
select * from PRODUCTION;

-- But since you&#39;ve &quot;consumed&quot; the stream (advanced its position by using rows
-- in a DML transaction), this will show no rows:
select * from STAGING_STREAM;

-- Why is this? It&#39;s helpful to think of the existence of rows in a stream (strictly
-- speaking, rows past the last consumed position of the stream) as an indication
-- that there have been unprocessed changes in your change data capture stream.
-- To see how this works, let&#39;s make some more changes:

-- Update a row to see how the stream responds:
update STAGING 
set FINAL_VALUE = &#39;Updated Row 1 Value&#39;, 
    HOW_CHANGED = &#39;Updated in change 4&#39;
where ID = 1;

-- Examine the change in the staging table:
select * from STAGING;

-- Since the last time you consumed the stream, you have one UPDATE to process.
-- Let&#39;s see what that looks like in the stream:
select * from STAGING_STREAM;

-- There are *two* rows. Why is that? The reason is how Snowflake processes updates.
-- In order to enable Snowflake Time Travel and for technical reasons, Snowflake
-- processes an UPDATE as a DELETE and an INSERT. Note that we can tell this is
-- an update, because there&#39;s another column, &quot;METADATA$ISUPDATE&quot; set to TRUE.
-- Let&#39;s process this change. We&#39;ll start with the DELETE first:
delete from PRODUCTION
where ID in (select ID from STAGING_STREAM where METADATA$ACTION = &#39;DELETE&#39;);

-- We&#39;ve now deleted row ID 1, let&#39;s check it and then do the INSERT:
select * from PRODUCTION;

-- But wait... What happened to the stream? Did it clear out only the DELETE 
-- metadata action because that&#39;s the only one you used in the DML?

select * from STAGING_STREAM;

-- Answer: ** No **. Even though you didn&#39;t use every row in the stream, *any*
-- DML transaction advances the stream to the end of the last change capture.
-- You could use &quot;begin&quot; and &quot;end&quot; to do the INSERT and DELETE one after the
-- other, or we could use UPDATE by checking the &quot;METADATA$ISUPDATE&quot;, but I&#39;d
-- like to propose a better, general-purpose solution: MERGING from the stream.

-- Let&#39;s see how this works. First, let&#39;s get the PRODUCTION table back in sync
-- with the STAGING table:
delete from PRODUCTION;
insert into PRODUCTION select * from STAGING;

-- Now, let&#39;s do an INSERT, UPDATE, and DELETE before &quot;consuming&quot; the stream
select * from STAGING;

insert into STAGING (ID, CHANGE_NUMBER, HOW_CHANGED, FINAL_VALUE)
  values (4, &#39;5th change to STAGING table&#39;, &#39;Inserted in change 5&#39;, &#39;Original Row 5 value&#39;);
  
update STAGING 
  set CHANGE_NUMBER = &#39;6th change to STAGING table&#39;, HOW_CHANGED = &#39;Updated in change 6&#39;
  where ID = 2;

delete from STAGING where ID = 3;

-- Now your STAGING and PRODUCTION tables are out of sync. The stream captures
-- all changes (change data capture or CDC) needed to process to get the tables
-- in sync:
select * from STAGING_STREAM;

-- Note that we have *FOUR* rows after making one change for each verb 
-- INSERT, UPDATE, and DELETE. Recall that Snowflake processes an UPDATE as
-- a DELETE followed by an INSERT, and shows this in the METADATA$ISUPDATE
-- metadata column. 

-- What if all you want to do is keep PROD in sync with STAGING, but control
-- when those changes happen and have the option to examine them before 
-- applying them? This next DML statement serves as a template to make this
-- use case super easy and efficient:

-- Let&#39;s look at the PRODUCTION table first:
select * from PRODUCTION;

-- Merge the changes from the stream. The graphic below this SQL explains 
-- how this processes all changes in one DML transaction.
merge into PRODUCTION P using
  (select * from STAGING_STREAM where METADATA$ACTION &amp;lt;&gt; &#39;DELETE&#39; or METADATA$ISUPDATE = false) S on P.ID = S.ID
    when matched AND S.METADATA$ISUPDATE = false and S.METADATA$ACTION = &#39;DELETE&#39; then 
      delete
    when matched AND S.METADATA$ISUPDATE = true then 
      update set P.ID = S.ID, 
                 P.CHANGE_NUMBER = S.CHANGE_NUMBER, 
                 P.HOW_CHANGED = S.HOW_CHANGED, 
                 P.FINAL_VALUE = S.FINAL_VALUE
    when not matched then 
      insert (ID, CHANGE_NUMBER, HOW_CHANGED, FINAL_VALUE) 
      values (S.ID, S.CHANGE_NUMBER, S.HOW_CHANGED, S.FINAL_VALUE);
      
-- Recall that you did 1 INSERT, 1 UPDATE, and 1 DELETE. The stream captured
-- all three changes, and the MERGE statement above performed all three in one
-- step. Now the PRODUCTION table is in sync with STAGING:
select * from PRODUCTION;

-- We consumed the stream, so it&#39;s advanced past any changes to show there&#39;s
-- nothing remaining to process:
select * from STAGING_STREAM;

-- We can process CDC streams any way we want, but to synchronize a staging
-- and production table, this MERGE template works great.

-- BTW, here&#39;s a handy trick to see if two tables that are supposed to be in
-- sync actually are in sync. There&#39;s a complete post on it here:
-- /index.php/2020/01/08/field-comparisons-using-snowflake/
-- This query will find any mismatched rows:
select P.ID            as P_ID,
       P.CHANGE_NUMBER as P_CHANGE_NUMBER,
       P.HOW_CHANGED   as P_HOW_CHANGED,
       P.FINAL_VALUE   as P_FINAL_VALUE,
       S.ID            as S_ID,
       S.CHANGE_NUMBER as S_CHANGE_NUMBER,
       S.HOW_CHANGED   as S_HOW_CHANGED,
       S.FINAL_VALUE   as S_FINAL_VALUE
from PRODUCTION P
full outer join STAGING S
on P.ID            = S.ID            and
   P.CHANGE_NUMBER = S.CHANGE_NUMBER and
   P.HOW_CHANGED   = S.HOW_CHANGED   and
   P.FINAL_VALUE   = S.FINAL_VALUE 
where P.ID            is null or
      S.ID            is null or
      P.CHANGE_NUMBER is null or
      S.CHANGE_NUMBER is null or
      P.HOW_CHANGED   is null or
      S.HOW_CHANGED   is null or
      P.FINAL_VALUE   is null or
      S.FINAL_VALUE   is null;&lt;/pre&gt;
&lt;p&gt;Think of the final MERGE statement as a general-purpose way to merge changes from any staging table&#39;s stream to its corresponding production table. In order to do that; however, you&#39;ll need to modify the template a bit. The following graphic should help explain what&#39;s going on here:&lt;/p&gt;
&lt;figure class=&quot;wp-block-image size-large&quot;&gt;&lt;img src=&quot;https://snowflake.pavlik.us/wp-content/uploads/2020/01/Merge_Using_Stream-1024x503.png&quot; alt=&quot;Merge Using Streams&quot; class=&quot;wp-image-235&quot; /&gt;&lt;figcaption&gt;Merge Using Stream - Annotated SQL&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Field Comparisons Using Snowflake</title>
    <link href="https://snowflake.pavlik.us/index.php/2020/01/08/field-comparisons-using-snowflake/"/>
    <updated>2020-01-08T00:00:00Z</updated>
    <id>https://snowflake.pavlik.us/index.php/2020/01/08/field-comparisons-using-snowflake/</id>
    <content type="html">&lt;h4 class=&quot;wp-block-heading&quot;&gt;Use cases for bulk field comparisons&lt;/h4&gt;
&lt;p&gt;There are a lot of reasons why it may be necessary to compare the values of some but not all fields in two tables. In billing reconciliation, one table may contain raw line items, and another table may contain lines in billing statements. Another common reason would be to verify that fields are accurate after undergoing transformation from the source system to a table optimized for analytics. &lt;/p&gt;
&lt;p&gt;Obviously when moving and transforming data a lot can happen along the way. Probably the most common problem is missing rows, but there are any number of other problems: updates out of sync, data corruption, transformation logic errors, etc.&lt;/p&gt;
&lt;h4 class=&quot;wp-block-heading&quot;&gt;How to compare fields across tables in Snowflake&lt;/h4&gt;
&lt;p&gt;Fortunately, Snowflake&#39;s super-fast table joining provides a great way to check for missing rows or differences in key fields between the tables. Without getting into Venn diagrams with inner, outer, left, right, etc., suffice it to say we&#39;re going to discuss what is perhaps the least used join: the &lt;strong&gt;full outer exclusive of inner join&lt;/strong&gt;. I&#39;ve seen this type of join called other names, but this is what it does:&lt;/p&gt;
&lt;figure class=&quot;wp-block-image size-full is-resized&quot;&gt;&lt;img src=&quot;https://snowflake.pavlik.us/wp-content/uploads/2020/01/outer_join_excluding_middle.png&quot; alt=&quot;&quot; class=&quot;wp-image-530&quot; style=&quot;width:478px;height:auto&quot; /&gt;&lt;/figure&gt;
&lt;p&gt;Think of it this way for the present use case: The excluded inner join excludes the rows with key fields that compare properly. In other words, if our reconciliation process on key fields between tables A and B is perfect, an inner join will return all rows. Turning that on its head, the inverse of an inner join (a full join exclusive of inner join) will return only the rows that have key field compare mismatches.&lt;/p&gt;
&lt;h4 class=&quot;wp-block-heading&quot;&gt;Snowflake performance for massive-scale field comparisons&lt;/h4&gt;
&lt;p&gt;The TPCH Orders tables used as a source has 150 million rows in it. Using this approach to compare four field values on 150 million rows, the equivalent of doing 600 million comparisons completed in ~12 seconds on an extra large cluster. This level of performance exceeds by orders of magnitude typical approaches such as using an ETL platform to perform comparisons and write a table of mismatched rows.&lt;/p&gt;
&lt;p&gt;We can see how this works in the following Snowflake worksheet:&lt;/p&gt;
&lt;pre class=&quot;wp-block-syntaxhighlighter-code&quot;&gt;-- Set the context
use warehouse TEST;
use database TEST;
create or replace schema FIELD_COMPARE;
use schema FIELD_COMPARE;

-- Note: This test goes more quickly and consumes the same number of credits by temporarily
--       scaling the TEST warehouse to extra large. The test takes only a few minutes.
--       Remember to set the warehouse to extra small when done with the table copies and query.
--       The test will work on an extra small warehouse, but it will run slower and consume the
--       same number of credits as running on an extra large and finishing quicker.
alter warehouse TEST set warehouse_size = &#39;XLARGE&#39;;

-- Get some test data, in this case 150 million rows from TPCH Orders
create table A as select * from SNOWFLAKE_SAMPLE_DATA.TPCH_SF100.ORDERS;
create table B as select * from SNOWFLAKE_SAMPLE_DATA.TPCH_SF100.ORDERS;

-- Quick check to see if the copy looks right.
select * from A limit 10;
select * from B limit 10;

-- We will now check to be sure the O_ORDERKEY, O_CUSTKEY, O_TOTALPRICE, and O_ORDERDATE fields
-- compare properly between the two tables. The query result will be any comparison problems.
-- Because we have copied table A and B from the same source, they should be identical. We expect
-- That the result set will have zero rows. 
select A.O_ORDERKEY      as A_ORDERKEY,
       A.O_CUSTKEY       as A_CUSTKEY,
       A.O_ORDERSTATUS   as A_ORDERSTATUS,
       A.O_TOTALPRICE    as A_TOTALPRICE,
       A.O_ORDERDATE     as A_ORDERDATE,
       A.O_ORDERPRIORITY as A_ORDERPRIORITY,
       A.O_CLERK         as A_CLERK,
       A.O_SHIPPRIORITY  as A_SHIPPRIORITY,
       A.O_COMMENT       as A_COMMENT,
       B.O_ORDERKEY      as B_ORDERKEY,
       B.O_CUSTKEY       as B_CUSTKEY,
       B.O_ORDERSTATUS   as B_ORDERSTATUS,
       B.O_TOTALPRICE    as B_TOTALPRICE,
       B.O_ORDERDATE     as B_ORDERDATE,
       B.O_ORDERPRIORITY as B_ORDERPRIORITY,
       B.O_CLERK         as B_CLERK,
       B.O_SHIPPRIORITY  as B_SHIPPRIORITY,
       B.O_COMMENT       as B_COMMENT
from A
full outer join B
on A.O_ORDERKEY   = B.O_ORDERKEY   and
   A.O_CUSTKEY    = B.O_CUSTKEY    and
   A.O_TOTALPRICE = B.O_TOTALPRICE and
   A.O_ORDERDATE  = B.O_ORDERDATE 
where A.O_ORDERKEY   is null or 
      B.O_ORDERKEY   is null or
      A.O_CUSTKEY    is null or
      B.O_CUSTKEY    is null or
      A.O_TOTALPRICE is null or
      B.O_TOTALPRICE is null or
      A.O_ORDERDATE  is null or
      B.O_ORDERDATE  is null;

-- Now we want to start changing some data to show comparison problems and the results. Here are some ways to do it.

-- Get two random clerks
select * from B tablesample(2);

-- Count the rows for these clerks - it should be about 3000 rows 
select count(*) from B where O_CLERK = &#39;Clerk#000065876&#39; or O_CLERK = &#39;Clerk#000048376&#39;; 

-- Now we can force some comparison problems. Perform one or more of the following:
-- NOTE: *** Do one or more of the following three changes or choose your own to force comparison problems. ***

-- Force comparison problem 1: Change about 3000 rows to set the order price to zero.
update B set O_TOTALPRICE = 0 where where O_CLERK = &#39;Clerk#000065876&#39; or O_CLERK = &#39;Clerk#000048376&#39;;

-- Force comparison problem 2: Delete about 3000 rows.
delete from B where O_CLERK = &#39;Clerk#000065876&#39; or O_CLERK = &#39;Clerk#000048376&#39;;

-- Force comparison problem 3: Insert a new row in only one table.
insert into B (O_ORDERKEY, O_CUSTKEY, O_ORDERSTATUS, O_TOTALPRICE, O_ORDERDATE) values (12345678, 12345678, &#39;O&#39;, 99.99, &#39;1999-12-31&#39;);

-- Now run the same join above to see the results. You if you make any changes to the joined fields, you should see rows.
-- INSERT of a row to table B will show up as ONE row, A side all NULL and B side with values.
-- UPDATE of a row to table B will show up as TWO rows, one row A side with values and B side with all NULL, and one row the other way
-- DELETE of a row in table B will show up as ONE row, values in the A side and B side all NULL 

-- Clean up:
drop table A;
drop table B;
drop schema FIELD_COMPARE;
alter warehouse TEST set warehouse_size = &#39;XSMALL&#39;;
alter warehouse TEST suspend;

&lt;/pre&gt;
</content>
  </entry>
</feed>
