Can Snowflake DB UDF's execute SQL commands? - sql

I have a requirement in Snowflake where I must generate a bit of SQL and then execute it to create a new table.
I have successfully generated the create table statement by creating a UDF (hard-coded at the moment)
CREATE OR REPLACE FUNCTION COOL_CARGO.test()
RETURNS STRING
AS
$$
SELECT substr(regexp_replace(GET_DDL('TABLE', 'COOL_CARGO.DIM_BRANCH'),('DIM_BRANCH'),'COOL_CARGO.DIM_BRANCH_ERR'), 0, LENGTH(regexp_replace(GET_DDL('TABLE', 'COOL_CARGO.DIM_BRANCH'),('DIM_BRANCH'),'COOL_CARGO.DIM_BRANCH_ERR')) -2)||','||' etl_err_date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
etl_id_run int DEFAULT NULL,
etl_err_noe int DEFAULT NULL,
etl_err_desc varchar(512) DEFAULT NULL,
etl_err_col varchar(256) DEFAULT NULL,
etl_err_cod varchar(256) DEFAULT NULL'||');'
$$
;
This outputs the following
create or replace TABLE COOL_CARGO.DIM_BRANCH_ERR (
TK_BRANCH NUMBER(38,0),
GB_BRANCH_CODE VARCHAR(256),
GB_BRANCH_NAME VARCHAR(256),
GB_BRANCH_CITY VARCHAR(256),
GB_BRANCH_STATE VARCHAR(256),
BG_BRANCH_HOME_PORT VARCHAR(256),
BG_BRANCH_COUNTRY_CODE VARCHAR(256),
BG_BRANCH_COUNTRY_NAME VARCHAR(256)
, etl_err_date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
etl_id_run int DEFAULT NULL,
etl_err_noe int DEFAULT NULL,
etl_err_desc varchar(512) DEFAULT NULL,
etl_err_col varchar(256) DEFAULT NULL,
etl_err_cod varchar(256) DEFAULT NULL);
I now need to create a UDF that will execute this create table statement but as it only seems to return things like strings, I cannot get it to execute by calling it from another function for example.
CREATE OR REPLACE FUNCTION COOL_CARGO.run_test()
RETURNS string
AS
$$
COOL_CARGO.test()
$$
;
Then I try and run the function to create the table with
select COOL_CARGO.run_test();
I do not know if what I want can be done and I would be pretty annoyed if its not possible...
Can this be done in Snowflake DB?

You can achieve this with Snowflake's new Stored Procedures feature that launched in 2019. It allows you to build an arbitrary SQL string and perform an execution in either SQL or JavaScript.
Your existing method can easily be adapted into a stored procedure (example below is illustrative and hasn't been specifically tested):
create or replace procedure test()
returns null
language javascript
as
$$
var orig_ddl_qry = "SELECT GET_DDL('TABLE', 'COOL_CARGO.DIM_BRANCH')";
var get_ddl_stmt = snowflake.createStatement(
{
sqlText: orig_ddl_qry
}
);
var get_ddl_res = get_ddl_stmt.execute();
get_ddl_res.next();
var orig_ddl_str = res.getColumnValue(1);
var replaced_ddl_open = orig_ddl_str.replace("DIM_BRANCH", "COOL_CARGO.DIM_BRANCH_ERR").slice(0, -1);
var new_ddl = replaced_ddl_open + ", etl_err_date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, etl_id_run int DEFAULT NULL, etl_err_noe int DEFAULT NULL, etl_err_desc varchar(512) DEFAULT NULL, etl_err_col varchar(256) DEFAULT NULL, etl_err_cod varchar(256) DEFAULT NULL);";
var create_stmt = snowflake.createStatement(
{
sqlText: new_ddl
}
);
var create_ddl_res = create_stmt.execute();
$$
;
CALL test();

Related

Add a trigger on select

Is it possible to create a select trigger? I know there are update insert or delete triggers. But select trigger is what I need for a little url shortener application. Everytime a shorturl is hit I'd like to update counter and last access date. A select trigger would be perfect.
select url where code = :code
whould be the one to trigger the trigger.
Ended up doing this:
create function `get_the_url`(c char(7))
returns varchar(255)
begin
declare result varchar(255);
update code
set
hits = hits + 1,
last_used=current_timestamp()
where code = c;
return (
select url
from code
where code = c
);
end
saving this for prosperity and me.
for this table:
create table `code` (
`code` char(5) not null,
`url` varchar(240) default null,
`last_used` datetime not null default '0000-00-00 00:00:00',
`hits` int(10) unsigned not null default 0,
primary key (`code`) using hash,
key `url` (`url`) using hash
) engine=myisam
For those few php folks interested, this is how I populated the table:
<?php
declare(strict_types=1);
$charset ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNAOPQRSTUVWXYZ1234567890-_.!~*'()";
$size = 8;
function getCode(): string {
global $size;
global $charset;
$length = strlen($charset)-1;
$result =
$charset[rand(0,$length)]
.$charset[rand(0,$length)]
.$charset[rand(0,$length)]
.$charset[rand(0,$length)]
.$charset[rand(0,$length)];
return $result;
}
$db = new \PDO("mysql:dbname=qr","qr","qr");
$insert = $db-> prepare("insert into code(code,url,last_used,hits)values(:code,null,default,default)");
$code = getCode();
$insert->bindParam(':code',$code);
for($i=1000; $i>0; $i--) {
$code = getCode();
$insert-> execute() or die($insert->errorInfo()[2]);
}
and yes, populating this upfront probably makes sense as storage space is cheap and computing power is not.

POST method in RESTfull problem with the sql or with the Statement to generate id?

GET, PUT and DELETE work, but trying to do the POST method it doesn't work. In my data base, 'usuario' has an id, a name and age (edad).
#POST
#Consumes({MediaType.APPLICATION_XML,MediaType.APPLICATION_JSON})
public Response crearUsuario(Usuario usuario) {
try {
String sql = "INSERT INTO `RedLibros`.`usuario`(`nombre`,`edad`) " + "VALUES('"
+ usuario.getnombre() + "', '" + usuario.getedad() + "');";
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
int affectedRows = ps.executeUpdate();
ResultSet generatedID = ps.getGeneratedKeys();
if (generatedID.next()) {
usuario.setidusuario(generatedID.getInt(1));
String location = uriInfo.getAbsolutePath() + "/" + usuario.getidusuario();
return Response.status(Response.Status.CREATED).entity(usuario).header("Location", location).header("Content-Location", location).build();
}
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("No se pudo crear el usuario").build();
} catch (SQLException e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("No se pudo crear el usuario\n" + e.getStackTrace()).build();
}
}
I'm using Postman and the body of my input is:
</usuario idusuario="1">
<edad>43</edad>
<nombre>George</nombre>
</usuario>
The error is: Ljava.lang.StackTraceElement;#1aeb297
And this is the script of my db, im not sure if its because the character set, im using UTF-8.
-- MySQL Script generated by MySQL Workbench
-- lun 19 abr 2021 14:13:50 CEST
-- Model: New Model Version: 1.0
SET #OLD_UNIQUE_CHECKS=##UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET #OLD_FOREIGN_KEY_CHECKS=##FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET #OLD_SQL_MODE=##SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';
-- -----------------------------------------------------
-- Schema RedLibros
-- -----------------------------------------------------
CREATE SCHEMA IF NOT EXISTS `RedLibros` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ;
USE `RedLibros` ;
-- -----------------------------------------------------
-- Table `RedLibros`.`usuario`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `RedLibros`.`usuario` (
`idusuario` INT NOT NULL,
`nombre` VARCHAR(45) NULL,
`edad` VARCHAR(45) NULL,
PRIMARY KEY (`idusuario`))
ENGINE = InnoDB;
I think you are not passing idusuario from POST call. If you expect this to be auto increment, then you need to define the table accordingly. You should use auto_increment syntax like below -
CREATE TABLE IF NOT EXISTS RedLibros.usuario ( idusuario INT NOT NULL AUTO_INCREMENT, nombre VARCHAR(45) NULL, edad VARCHAR(45) NULL, PRIMARY KEY (idusuario));

SQL stored procedure call failed with index out of range

I am trying to execute a stored procedure in SQL Server from my java code. This stored procedure is used in some other language and it was working fine from long back. Now I need to integrate it in my java app. I have 15 columns in my table. When I tried this in my java code, its throwing
com.microsoft.sqlserver.jdbc.SQLServerException: The index 11 is out of range
I also see "Error: 0, SQLState: S1093"
My stored procedure
ALTER PROCEDURE [dbo].[user_input_sp]
#Username VARCHAR(10)=NULL,
#UserKey VARCHAR(50)=NULL,
#ReqTime DATETIME=null,
#ResTime DATETIME=null,
#Req TEXT=null,
#Res TEXT=null,
#condition TEXT=null,
#ID INT=null,
#Address VARCHAR(8000) = NULL,
#Task VARCHAR(50) = NULL,
#Direction INT=0,
#Seq INT=0,
#RR BIT=0,
#Correction VARCHAR(8) = NULL,
#PendingTrans VARCHAR(50) = NULL,
#ForwardID VARCHAR(50) = NULL,
#Command VARCHAR(50) = NULL
AS
DECLARE #TSqno int
#IF #Direction=0
BEGIN
EXECUTE Basp_NewKey 'web_log', #TSqno OUTPUT
Insert into cbscne dbo WebServiceLog( Order, US_Name, US_Key , US_ReqD, US_ResD, US_Req , US_Res, cond, ID ,Address, Task, Command)
values (#TSqno, #Username, #UserKey, #ReqTime, #ResTime, #Req ,#Res, #condition, #ID ,#Address, #Task ,#Command)
END
.......
My java code
public vold addWebServiceLogRequest(UserInput cd){
sessionFactory = sqlServerEMFactory.unwrap(SessionFactory.class);
Session sessionForSave = sessionFactory.openSession();
sessionForSave.beginTransaction();
sessionForSave.doReturningWork(connection-> {
try(CallableStatement cstmt = connection.prepareCall("{call user_input_sp(?,?,?,?,?,?,?,?,?)}")) {
cstmt.setInt("indicator", 0);
cstmt.setString("Username",cd.getInterfaceName());
cstmt.setInt("User_code",cd.getCaseId());
cstmt.setTimestamp("RetrieveDate",cd.getRequestDate());
cstmt.setTimestamp("ReturnDate",cd.getResponseDate());
cstmt.setString("Req",cd.getRequestxml());
cstmt.setString("Res",cd.getResponsexml());
cstmt.setString("Task",cd.getTask());
cstmt.setString("flow",null);
cstmt.execute();
cstmt.close();
connection.close();
return null;
}
});
}

Although I send the parameter I get "Can not insert NULL value..."

I am developing an ADO.NET application. At some point in the DAL I call a stored-procedure named "CREATE_CUSTOMER". Although I set the SHORT_NAME field I still get the
"Msg 515, Level 16, State 2, Procedure CREATE_CUSTOMER, Line 29
Cannot insert the value NULL into column 'SHORT_NAME', table 'MYDB.app.CUSTOMER';
column does not allow nulls. INSERT fails." error.
When I inspect the query with the SQL profiler I get the following SQL runs on the server. As I Copy&Paste it to a new Query Window I still get the same error.
Do I miss something?
declare #p16 int
set #p16=NULL
exec sp_executesql N'[app].[CREATE_CUSTOMER]',
N'#SHORT_NAME nvarchar(11),
#MAIL_NAME nvarchar(18),
#MT_SALESPERSON_ID int,
#CREDIT_LIMIT decimal(1,0),
#CREDIT_LIMIT_CURRENCY_ID int,
#PAYMENT_TYPE_ID int,
#SALES_TERM_ID int,
#FREE_STORAGE_DAY_ID int,
#RISK_GROUP_ID int,
#SECTOR_ID int,
#OCCUPATION_ID int,
#STORAGE_FEE_ID int,
#STATUS smallint,
#IDENTITY int output',
#SHORT_NAME=N'NEW Corp',
#MAIL_NAME=N'NEW Corporation',
#MT_SALESPERSON_ID=3,
#CREDIT_LIMIT=0,
#CREDIT_LIMIT_CURRENCY_ID=1,
#PAYMENT_TYPE_ID=4,
#SALES_TERM_ID=7,
#FREE_STORAGE_DAY_ID=6,
#RISK_GROUP_ID=3,
#SECTOR_ID=13,
#OCCUPATION_ID=16,
#STORAGE_FEE_ID=6,
#STATUS=0,
#IDENTITY=#p16 output
select #p16
And my Stored Procedure is as follows :
CREATE PROCEDURE [app].[CREATE_CUSTOMER]
#SHORT_NAME varchar(250) = NULL,
#MAIL_NAME varchar(500) = NULL,
#MT_SALESPERSON_ID int = NULL,
#CREDIT_LIMIT decimal(18,2) = NULL,
#CREDIT_LIMIT_CURRENCY_ID int = NULL,
#PAYMENT_TYPE_ID int = NULL,
#SALES_TERM_ID int = NULL,
#FREE_STORAGE_DAY_ID int = NULL,
#RISK_GROUP_ID int = NULL,
#SECTOR_ID int = NULL,
#OCCUPATION_ID int = NULL,
#STORAGE_FEE_ID int = NULL,
#STATUS tinyint = NULL,
#IDENTITY INT = NULL OUTPUT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO [app].[CUSTOMER]
([SHORT_NAME],
[MAIL_NAME],
[MT_SALESPERSON_ID],
[CREDIT_LIMIT],
[CREDIT_LIMIT_CURRENCY_ID],
[PAYMENT_TYPE_ID],
[SALES_TERM_ID],
[FREE_STORAGE_DAY_ID],
[RISK_GROUP_ID],
[SECTOR_ID],
[OCCUPATION_ID],
[STORAGE_FEE_ID],
[STATUS],
[CREATE_DATE],
[CREATE_USERID])
VALUES
(#SHORT_NAME,
#MAIL_NAME,
#MT_SALESPERSON_ID,
#CREDIT_LIMIT,
#CREDIT_LIMIT_CURRENCY_ID,
#PAYMENT_TYPE_ID,
#SALES_TERM_ID,
#FREE_STORAGE_DAY_ID,
#RISK_GROUP_ID,
#SECTOR_ID,
#OCCUPATION_ID,
#STORAGE_FEE_ID,
#STATUS,
GETDATE(),
CONTEXT_INFO())
SELECT #IDENTITY = SCOPE_IDENTITY()
END
This SQL code is being generated by the ADO.NET. Actual C# code is :
private static ICustomer CreateCustomer(ICustomer customer, int contextUserId)
{
try
{
string sql = "[app].[CREATE_CUSTOMER]";
SqlConnection conn = null;
using (conn = GetConnection())
{
SetContextInfomationToConnection(conn, contextUserId);
SqlCommand cmd = new SqlCommand(sql, conn);
cmd.Parameters.AddWithValue("#SHORT_NAME", customer.ShortName);
cmd.Parameters.AddWithValue("#MAIL_NAME", customer.MailName);
cmd.Parameters.AddWithValue("#MT_SALESPERSON_ID", customer.SalesPersonId);
cmd.Parameters.AddWithValue("#CREDIT_LIMIT", customer.CreditLimit);
cmd.Parameters.AddWithValue("#CREDIT_LIMIT_CURRENCY_ID", customer.CreditLimitCurrencyId);
cmd.Parameters.AddWithValue("#PAYMENT_TYPE_ID", customer.PaymentTypeId);
cmd.Parameters.AddWithValue("#SALES_TERM_ID", customer.SalesTermId);
cmd.Parameters.AddWithValue("#FREE_STORAGE_DAY_ID", customer.FreeStorageDayId);
cmd.Parameters.AddWithValue("#RISK_GROUP_ID", customer.RiskGroupId);
cmd.Parameters.AddWithValue("#SECTOR_ID", customer.SectorId);
cmd.Parameters.AddWithValue("#OCCUPATION_ID", customer.OccupationId);
cmd.Parameters.AddWithValue("#STORAGE_FEE_ID", customer.StorageFeeId);
cmd.Parameters.AddWithValue("#STATUS", customer.Status);
SqlParameter prmNewId = new SqlParameter("#IDENTITY", SqlDbType.Int, 4);
prmNewId.Direction = ParameterDirection.Output;
cmd.Parameters.Add(prmNewId);
cmd.ExecuteNonQuery();
int id = prmNewId.Value != DBNull.Value ? (int)prmNewId.Value : -1;
if (id > 0)
{
customer.Id = id;
return customer;
}
else
{
throw new Exception("Can not insert customer record with Id generation");
}
}
}
catch (Exception ex)
{
throw;
}
}
Your code is the equivalent of doing this:
DECLARE #SHORT_NAME nvarchar(11) = N'NEW Corp';
...
EXEC [app].[CREATE_CUSTOMER];
You are just declaring the parameters, never actually passing them to the procedure invocation. Your code should be like this:
exec sp_executesql N'[app].[CREATE_CUSTOMER] #SHORT_NAME, #MAIL_NAME, ...',
N'#SHORT_NAME nvarchar(11),
#MAIL_NAME nvarchar(18),
...',
#SHORT_NAME=N'NEW Corp',
#MAIL_NAME=N'NEW Corporation',
...
You must not only declare the parameters you pass to the batch, you must also use them when you invoke the procedure.
When I inspect the query with the SQL profiler I get the following SQL runs on the server. As I Copy&Paste it to a new Query Window I still get the same error
This sounds suspiciously like you are using a SqlCommand but forgot to set the CommandType to Procedure. the default is Text and will behave exactly as you observed.
Do not assign null values to ur variable, try only with DECLARING it as bellow
DECLARE #SHORT_NAME varchar(250) ,
instead of
#SHORT_NAME varchar(250) = NULL,

Return value from MySQL stored procedure

So I've finally decided to get around to learning how to use stored procedures, and although I do have them working, I'm unsure if I'm doing it correctly - aka. the best way. So here's what I've got.
Three procedures: TryAddTag, CheckTagExists, and AddTag.
TryAddTag is the procedure that is my intermediary between other code (eg. PHP, etc...) and the other two procedures, so this is the one that gets called.
TryAddTag
DELIMITER //
CREATE PROCEDURE TryAddTag(
IN tagName VARCHAR(255)
)
BEGIN
-- Check if tag already exists
CALL CheckTagExists(tagName, #doesTagExist);
-- If it does not exist, add it
IF #doesTagExist = FALSE THEN
CALL AddTag(tagName);
END IF;
END //
DELIMITER ;
AddTag
DELIMITER //
CREATE PROCEDURE AddTag(
IN tagName VARCHAR(255)
)
BEGIN
INSERT INTO
tags
VALUES(
NULL,
tagName
);
END //
DELIMITER ;
CheckTagExists
DELIMITER //
CREATE PROCEDURE CheckTagExists(
IN
tagName VARCHAR(255),
OUT
doesTagExist BOOL
)
BEGIN
-- Check if tag exists
SELECT
EXISTS(
SELECT
*
FROM
tags
WHERE
tags.NAME = tagName
)
INTO
doesTagExist;
END //
DELIMITER ;
My problems stem from this and use of #doesTagExist.
-- Check if tag already exists
CALL CheckTagExists(tagName, #doesTagExist);
Is the the correct way to use one of these variables? And/or, how can I use a DECLARE'd variable to store the result of CheckTagExists within TryAddTag? I expected something along the lines of
...
DECLARE doesTagExist BOOL;
SET doesTagExist = CheckTagExist('str');
...
or something like that...
your stored procedure is a little over-engineered for my liking - keep it simple :)
MySQL
drop table if exists tags;
create table tags
(
tag_id int unsigned not null auto_increment primary key,
name varchar(255) unique not null
)
engine=innodb;
drop procedure if exists insert_tag;
delimiter #
create procedure insert_tag
(
in p_name varchar(255)
)
proc_main:begin
declare v_tag_id int unsigned default 0;
if exists (select 1 from tags where name = p_name) then
select -1 as tag_id, 'duplicate name' as msg; -- could use multiple out variables...i prefer this
leave proc_main;
end if;
insert into tags (name) values (p_name);
set v_tag_id = last_insert_id();
-- do stuff with v_tag_id...
-- return success
select v_tag_id as tag_id, 'OK' as msg;
end proc_main #
delimiter ;
PHP
<?php
ob_start();
try{
$conn = new mysqli("localhost", "foo_dbo", "pass", "foo_db", 3306);
$conn->autocommit(FALSE); // start transaction
// create the tag
$name = 'f00';
$sql = sprintf("call insert_tag('%s')", $conn->real_escape_string($name));
$result = $conn->query($sql);
$row = $result->fetch_array();
$result->close();
$conn->next_result();
$tagID = $row["tag_id"]; // new tag_id returned by sproc
if($tagID < 0) throw new exception($row["msg"]);
$conn->commit();
echo sprintf("tag %d created<br/>refresh me...", $tagID);
}
catch(exception $ex){
ob_clean();
//handle errors and rollback
$conn->rollback();
echo sprintf("oops error - %s<br/>", $ex->getMessage());
}
// finally
$conn->close();
ob_end_flush();
?>
Stored PROCEDURES can return a resultset. The last thing you SELECT in a stored procedure is available as a resultset to the calling environment.. Stored FUNCTIONS can return only a single result primitive.
You may also mark your parameters as INOUT parameters.
If you want this:
DECLARE doesTagExist BOOL;
SET doesTagExist = CheckTagExist('str');
then you should use functions:
DELIMITER //
CREATE FUNCTION CheckTagExists(
tagName VARCHAR(255)
)
BEGIN
DECLARE doesTagExist BOOL;
-- Check if tag exists
SELECT
EXISTS(
SELECT
*
FROM
tags
WHERE
tags.NAME = tagName
)
INTO
doesTagExist;
RETURN doesTagExist;
END //
DELIMITER ;
DECLARE doesTagExist BOOL;
SET CheckTagExist('str',doesTagExist);
is the correct way of doing it with just store procedures. There are no 'regular' return values.