Stored procedure - get anticipated columns before fully executing statement? - snowflake-cloud-data-platform

I'm working through a stored procedure and wondering if there's a way to retrieve the anticipated result column list from a sql statement before fully executing.
Scenarios:
dynamic SQL
a UDF that might vary the columns outside of our control
EX:
//inbound parameter
SET QUERY_DEFINITION_ID = 12345;
//Initial statement pulls query text from bank of queries
var sqlText = getQueryFromQueryBank(QUERY_DEFINITION_ID);
//now we run our query
var cmd = {sqlText: sqlText };
stmt = snowflake.createStatement(cmd);
What I'd like to be able to do is say "right - before you run this, give me the anticipated column list" so I can compare it to what's expected.
EX:
Expected: [col1, col2, col3, col4]
Got: [col1]
Result: Oops. Don't run.
Rationale here is that I want to short-circuit the execution if something is missing - before it potentially runs for a while. I can validate all of this after the fact, but it would be really helpful to stop early.
Any ideas very much appreciated!

This sample SP code shows how to get a list of columns that a query will project into the result before you run the query. It should only be used for large, long running queries because it will take a few seconds to get the column list.
There are a couple of caveats. 1) It will only return the names of the columns. It won't tell you how they were built, that is, whether they're aliased, direct from a table, calculated, etc. 2) The example query I used is straight from the Snowflake documentation here https://docs.snowflake.com/en/user-guide/sample-data-tpcds.html#functional-query-definition. For convenience, I minimized the query to a single line. The output of the columns includes object qualifiers in addition to the column names, so V1.I_CATEGORY, V1.D_YEAR, V1.D_MOY, etc. If you don't want them to make it easier to compare names, you can strip off the qualifiers using the JavaScript split function on the dot and take index 1 of the resulting array.
create or replace procedure EXPLAIN_BEFORE_RUNNING()
returns string
language javascript
execute as caller
as
$$
// Set the context for the session to the TPC-H sample data:
executeNonQuery("use schema snowflake_sample_data.tpcds_sf10tcl;");
// Here's a complex query from the Snowflake docs (minimized to one line for convienience):
var sql = `with v1 as( select i_category, i_brand, cc_name, d_year, d_moy, sum(cs_sales_price) sum_sales, avg(sum(cs_sales_price)) over(partition by i_category, i_brand, cc_name, d_year) avg_monthly_sales, rank() over (partition by i_category, i_brand, cc_name order by d_year, d_moy) rn from item, catalog_sales, date_dim, call_center where cs_item_sk = i_item_sk and cs_sold_date_sk = d_date_sk and cc_call_center_sk= cs_call_center_sk and ( d_year = 1999 or ( d_year = 1999-1 and d_moy =12) or ( d_year = 1999+1 and d_moy =1)) group by i_category, i_brand, cc_name , d_year, d_moy), v2 as( select v1.i_category ,v1.d_year, v1.d_moy ,v1.avg_monthly_sales ,v1.sum_sales, v1_lag.sum_sales psum, v1_lead.sum_sales nsum from v1, v1 v1_lag, v1 v1_lead where v1.i_category = v1_lag.i_category and v1.i_category = v1_lead.i_category and v1.i_brand = v1_lag.i_brand and v1.i_brand = v1_lead.i_brand and v1.cc_name = v1_lag.cc_name and v1.cc_name = v1_lead.cc_name and v1.rn = v1_lag.rn + 1 and v1.rn = v1_lead.rn - 1) select * from v2 where d_year = 1999 and avg_monthly_sales > 0 and case when avg_monthly_sales > 0 then abs(sum_sales - avg_monthly_sales) / avg_monthly_sales else null end > 0.1 order by sum_sales - avg_monthly_sales, 3 limit 100;`;
// Before actually running the query, generate an explain plan.
executeNonQuery("explain " + sql);
// Now read the column list from the explain plan from the result set.
var columnList = executeSingleValueQuery("COLUMN_LIST", `select "expressions" as COLUMN_LIST from table(result_scan(last_query_id())) where "operation" = 'Result';`);
// For now, just exit with the column list as the output...
return columnList;
// Your code here...
// Helper functions:
function executeNonQuery(queryString) {
var out = '';
cmd = {sqlText: queryString};
stmt = snowflake.createStatement(cmd);
var rs;
rs = stmt.execute();
}
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) == "ResultSet is empty"){
throw "ERROR: No rows returned in query.";
} else {
throw "ERROR: " + err.message.replace(/\n/g, " ");
}
}
return out;
}
$$;
call Explain_Before_Running();

Related

How to generate Stackoverflow table markdown from Snowflake

Stackoverflow supports table markdown. For example, to display a table like this:
N_NATIONKEY
N_NAME
N_REGIONKEY
0
ALGERIA
0
1
ARGENTINA
1
2
BRAZIL
1
3
CANADA
1
4
EGYPT
4
You can write code like this:
|N_NATIONKEY|N_NAME|N_REGIONKEY|
|---:|:---|---:|
|0|ALGERIA|0|
|1|ARGENTINA|1|
|2|BRAZIL|1|
|3|CANADA|1|
|4|EGYPT|4|
It would save a lot of time to generate the Stackoverflow table markdown automatically when running Snowflake queries.
The following stored procedure accepts either a query string or a query ID (it will auto-detect which it is) and returns the table results as Stackoverflow table markdown. It will automatically align numbers and dates to the right, strings, arrays, and objects to the left, and other types default to centered. It supports any query you can pass to it. It may be a good idea to use $$ to terminate the string passed into the procedure in case the SQL contains single quotes. You can create the procedure and test it using this script:
create or replace procedure MARKDOWN("queryOrQueryId" string)
returns string
language javascript
execute as caller
as
$$
const MAX_ROWS = 50; // Set the maximum row count to fetch. Tables in markdown larger than this become hard to read.
var [rs, i, c, row, props] = [null, 0, 0, 0, {}];
if (!queryOrQueryId || queryOrQueryId == 0){
queryOrQueryId = `select * from table(result_scan(last_query_id())) limit ${MAX_ROWS}`;
}
queryOrQueryId = queryOrQueryId.trim();
if (isUUID(queryOrQueryId)){
rs = snowflake.execute({sqlText:`select * from table(result_scan('${queryOrQueryId}')) limit ${MAX_ROWS}`});
} else {
rs = snowflake.execute({sqlText:`${queryOrQueryId}`});
}
props.columnCount = rs.getColumnCount();
for(i = 1; i <= props.columnCount; i++){
props["col" + i + "Name"] = rs.getColumnName(i);
props["col" + i + "Type"] = rs.getColumnType(i);
}
var table = getHeader(props);
while(rs.next()){
row = "|";
for(c = 1; c <= props.columnCount; c++){
row += escapeMarkup(rs.getColumnValueAsString(c)) + "|";
}
table += "\n" + row;
}
return table;
//------ End main function. Start of helper functions.
function escapeMarkup(s){
s = s.replace(/\\/g, "\\\\");
s = s.replaceAll('|', '\\|');
s = s.replace(/\s+/g, " ");
return s;
}
function getHeader(props){
s = "|";
for (var i = 1; i <= props.columnCount; i++){
s += props["col" + i + "Name"] + "|";
}
s += "\n";
for (var i = 1; i <= props.columnCount; i++){
switch(props["col" + i + "Type"]) {
case 'number':
s += '|---:';
break;
case 'string':
s += '|:---';
break;
case 'date':
s += '|---:';
break;
case 'json':
s += '|:---';
break;
default:
s += '|:---:';
}
}
return s + "|";
}
function isUUID(str){
const regexExp = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi;
return regexExp.test(str);
}
$$;
-- Usage type 1, a simple query:
call stackoverflow_table($$ select * from SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.NATION limit 5 $$);
-- Usage type 2, a query ID:
select * from SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.NATION limit 5;
set quid = (select last_query_id());
call stackoverflow_table($quid);
Edit: Based on Fieldy's helpful feedback, I modified the procedure code to allow passing null or 0 or a blank string '' as the parameter. This will use the last query ID and is a helpful shortcut. It also adds a constant to the code that will limit the returns to a set number of rows. This limit will be applied when using query IDs (or sending null, '', or 0, which uses the last query ID). The limit is not applied when the input parameter is the text of a query to run to avoid syntax errors if there's already a limit applied, etc.
Greg Pavlik's Javascript Stored Procedure solution made me wonder if this would be any easier with the new Python language support in Stored Procedures. This is currently a public-preview feature.
The Python Snowpark API supports returning a result as a Pandas dataframe, and Pandas supports returning a dataframe in Markdown format, via the tabulate package. Here's the stored procedure.
CREATE OR REPLACE PROCEDURE markdown_table(query_id VARCHAR)
RETURNS VARCHAR
LANGUAGE PYTHON
RUNTIME_VERSION = '3.8'
PACKAGES = ('snowflake-snowpark-python','pandas','tabulate', 'regex')
HANDLER = 'markdown_table'
EXECUTE AS CALLER
AS $$
import pandas as pd
import tabulate
import regex
def markdown_table(session, queryOrQueryId = None):
# Validate UUID
if(queryOrQueryId is None):
pandas_result = session.sql("""Select * from table(result_scan(last_query_id()))""").to_pandas()
elif(bool(regex.match("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", queryOrQueryId))):
pandas_result = session.sql(f"""select * from table(result_scan('{queryOrQueryId}'))""").to_pandas()
else:
pandas_result = session.sql(queryOrQueryId).to_pandas()
return pandas_result.to_markdown()
$$;
Which you can use as follows:
-- Usage type 1, use the result from the query ran immediately proceeding the Store-Procedure Call
select * from SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.NATION limit 5;
call markdown_table(NULL);
-- Usage type 2, pass in a query_id
select * from SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.NATION limit 5;
set quid = (select last_query_id());
select $quid;
call markdown_table($quid);
-- Usage type 3, provide a Query string to the Store-Procedure Call
call markdown_table('select * from SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.NATION limit 5');
The table can also be
N_NATIONKEY|N_NAME|N_REGIONKEY
--|--|--
0|ALGERIA|0
1|ARGENTINA|1
2|BRAZIL|1
3|CANADA|1
4|EGYPT|4
giving, so it can be a simpler solution
N_NATIONKEY
N_NAME
N_REGIONKEY
0
ALGERIA
0
1
ARGENTINA
1
2
BRAZIL
1
3
CANADA
1
4
EGYPT
4
I grab the result table and use notepad++ and replace tab \t with pipe space | and then insert by hand the header marker line. I sometime replace the empty null results with the text null to make the results make more sense. the form you use with the start/end pipes gets around the need for that.
DBeaver IDE supports "data export as markdown" and "advanced copy as markdown" out-of-the-box:
Output:
|R_REGIONKEY|R_NAME|R_COMMENT|
|-----------|------|---------|
|0|AFRICA|lar deposits. blithely final packages cajole. regular waters are final requests. regular accounts are according to |
|1|AMERICA|hs use ironic, even requests. s|
|2|ASIA|ges. thinly even pinto beans ca|
|3|EUROPE|ly final courts cajole furiously final excuse|
|4|MIDDLE EAST|uickly special accounts cajole carefully blithely close requests. carefully final asymptotes haggle furiousl|
It is rendered as:
R_REGIONKEY
R_NAME
R_COMMENT
0
AFRICA
lar deposits. blithely final packages cajole. regular waters are final requests. regular accounts are according to
1
AMERICA
hs use ironic, even requests. s
2
ASIA
ges. thinly even pinto beans ca
3
EUROPE
ly final courts cajole furiously final excuse
4
MIDDLE EAST
uickly special accounts cajole carefully blithely close requests. carefully final asymptotes haggle furiousl

Capture and run result_scan using query_id in Snowflake Procedure

Trying to run Describe table and running RESULT_SCAN on the query id of the describe table query.
Procedure:
var qry = ` describe table TEST_TABLE `;
var qry_rslt = snowflake.execute({sqlText: qry});
var qry_id= qry_rslt.getQueryId();
var qry2 = ` select * from table(result_scan('`+qry_id+`')) `
snowflake.execute({sqlText: qry2});
The procedure is returning Null and not running the SQL. On manually running the result scan query it says statement not found.
ANy idea how to read describe result.
You're not actually reading the results of the second query. It's running it but not collecting the results. This will collect the first column only of the result set:
create or replace procedure test()
returns string
language javascript
as
$$
var qry = ` describe table TEST_TABLE `;
var qry_rslt = snowflake.execute({sqlText: qry});
var qry_id= qry_rslt.getQueryId();
var qry2 = ` select * from table(result_scan('${qry_id}')) `;
rs = snowflake.execute({sqlText: qry2});
var out = "";
var i = 0;
while (rs.next()) {
if (i++ > 0) out += ",";
out += rs.getColumnValue(1);
}
return out;
$$;
call test();
Are you looking to get the entire DDL in one statement? If so you can run get_ddl and then read just the first row, first column. It will have the DDL for the entire table. If you want it as a table, you'll need to read the rows and columns to do what needs to be done with them.

How to assign a sql query result to a variable in stored procedure

I am trying to assign a simple sql query result in stored procedure to a variable which I will use it later in another sql statement. If I execute below statement with out putting in procedure it works but not with stored procedure, I get an error while calling sp , Can someone please help me here ?
Procedure code:
$$
VAR NAME = 'ABC'
SET (COUNT_VALUE) = (SELECT COUNT(*) FROM COUNT_TABLE)
With out Procedure below code works
SET (COUNT_VALUE) = (SELECT COUNT(*) FROM COUNT_TABLE)
select $COUNT_VALUE
Thanks
Here's an example that uses a simple helper function to return a result set from a query. You can read the Snowflake docs to see what the result set API has in it. The getResultSet function returns a variable that has the results of the query. Since the SQL is a count, you need to use rs.next() once to get to the first row and then read the value in the aliased count(*). You could also use rs.getColumnValue(1) to get a column by ordinal position, but I recommend using column names with SQL aliases is necessary.
create or replace procedure COUNT_EXAMPLE()
returns string
language javascript
as
$$
var rowCount = 0;
var sql = "select count(*) as ROW_COUNT from SNOWFLAKE_SAMPLE_DATA.TPCH_SF10000.ORDERS;";
try {
var rs = getResultSet(sql);
if (rs.next()) {
var rowCount = rs.getColumnValue("ROW_COUNT");
} else {
return "Error: Count query failed.";
}
}
catch(err) {
return "Error: " + err.message;
}
return "The table has " + rowCount + " rows.";
//--------------------------- End of main function ---------------------------
function getResultSet(sql){
cmd1 = {sqlText: sql};
stmt = snowflake.createStatement(cmd1);
var rs;
rs = stmt.execute();
return rs;
}
$$;
call COUNT_EXAMPLE();

Snowflake Stored Procedure - how do I retrieve the number of rows created by a CTAS Statement

In a snowflake stored procedure I am executing CTAS statements and want to retrieve the number of rows in the resultant object. We don't have access to QUERY_HISTORY (we get an error), and RESULT_SCAN(LAST_QUERY_ID()) doesn't help either (it gives us back the Table xyz Created result, but does not have meta-data i.e. number of rows created).
I can do it with a Select Count(*) in a separate query, but that seems to be a hack since the Row Count is right there in the History.
CREATE OR REPLACE PROCEDURE EDW_ADMIN.DAG_TEST()
RETURNS VARCHAR(512)
LANGUAGE JAVASCRIPT
AS
$$
{
let strCTAS = "";
let rsCTAS;
let rsRowsAffected;
let rowsAffected = 0;
strCTAS = "CREATE OR REPLACE TABLE EDW_ADMIN.DEMO_PROC_TEMP AS SELECT * FROM RAW_BIR.H_RPTUNIT;";
rsCTAS = snowflake.execute( {sqlText: strCTAS} );
// This works in a Query Worksheet in the browser, but gives me the following error when called from a procedure
// "[Stored procedure execution error: Requested information on the current user is not accessible in stored procedure.]"
rsRowsAffected = snowflake.execute( {sqlText: "SELECT ROWS_PRODUCED FROM TABLE(INFORMATION_SCHEMA.QUERY_HISTORY(RESULT_LIMIT=>100)) WHERE QUERY_ID = LAST_QUERY_ID();"} );
rsRowsAffected.next();
rowsAffected = rsRowsAffected.getColumnValue(1);
// This works, but you are doing execute i/o which is really un-necessary
// rsRowsAffected = snowflake.execute( {sqlText: "SELECT COUNT(*) FROM EDW_ADMIN.DEMO_PROC_TEMP;"} );
// rsRowsAffected.next();
// rowsAffected = rsRowsAffected.getColumnValue(1);
// This does NOT work, RESULT_SCAN has no metadata associated with it, this returns "Table DEMO_PROC_TEMP successfully created."
// rsRowsAffected = snowflake.execute ( {sqlText: "SELECT * FROM TABLE(RESULT_SCAN(LAST_QUERY_ID()));" } );
// rsRowsAffected.next();
// rowsAffected = rsRowsAffected.getColumnValue(1);
return rowsAffected;
}
$$
;
CALL EDW_ADMIN.DAG_TEST();
DROP EDW_ADMIN.DEMO_PROC_TEMP;
DROP PROCEDURE EDW_ADMIN.DAG_TEST();
Try adding execute as caller to the stored procedure declaration. For example:
create or replace procedure p()
returns text
language javascript
execute as caller
as
$$
const stmt1 = snowflake.createStatement( { sqlText: "create or replace table t as select $1 x from values (1),(2),(3)" } )
const rs1 = stmt1.execute()
const stmt2 = snowflake.createStatement( { sqlText: "SELECT ROWS_PRODUCED FROM TABLE(INFORMATION_SCHEMA.QUERY_HISTORY(RESULT_LIMIT=>100)) WHERE QUERY_ID = LAST_QUERY_ID()" } )
const rs2 = stmt2.execute()
rs2.next()
const rowsAffected = rs2.getColumnValue(1)
return rowsAffected
$$
;
call p();
returns 3

Stream and Task Stored procedure example, I am not sure how to troubleshoot the javascript

I have just learned about Streams and Tasks in Snowflake and have tested the example for tracking members and sign ups in the first example here.
I have attempted to create a stored procedure that looks like this:
CREATE OR REPLACE procedure member_update()
returns string not null
language javascript
as
$$
var statement = snowflake.createStatement({sqlText: "BEGIN"} );
statement.execute();
var statement = snowflake.createStatement({sqlText: "UPDATE members SET fee = fee + 15 where fee > 0"} );
statement.execute();
var statement = snowflake.createStatement({sqlText: "SELECT * FROM member_check"} );
statement.execute();
var statement = snowflake.createStatement({sqlText: "COMMIT"} );
statement.execute();
var statement = snowflake.createStatement({sqlText: "SELECT * FROM member_check"} );
statement.execute();
$$
;
INSERT INTO members (id,name,fee)
VALUES
(11,'Bill',0),
(12,'Jason',0),
(13,'Suzan',0),
(14,'Michelle',0),
(15,'AARON',0);
SELECT * FROM MEMBER_CHECK;
INSERT INTO signup
VALUES
(11,'2018-01-01'),
(12,'2018-02-15'),
(13,'2018-05-01'),
(14,'2018-07-16'),
(15,'2018-08-21');
MERGE INTO members m
USING (
SELECT id, dt
FROM signup s
WHERE DATEDIFF(day, '2018-08-15'::date, s.dt::DATE) < -30) s
ON m.id = s.id
WHEN MATCHED THEN UPDATE SET m.fee = 90;
Call member_update();
I have run into an error and was hoping I might get some guidance on by very beginner stored procedure.
Error:
Execution error in store procedure MEMBER_UPDATE: empty argument
passed At Snowflake.createStatement, line 2 position 29

Resources