mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 08:04:25 +00:00
feat: add snowflake (#1987)
* feat: unveil windmill AI * feat: add snowflake * fix: uppercase snowflake auth params
This commit is contained in:
Generated
+49
-1
@@ -2651,6 +2651,20 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonwebtoken"
|
||||
version = "8.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378"
|
||||
dependencies = [
|
||||
"base64 0.21.2",
|
||||
"pem 1.1.1",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "konst"
|
||||
version = "0.2.19"
|
||||
@@ -3132,7 +3146,7 @@ dependencies = [
|
||||
"mysql_common",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"pem",
|
||||
"pem 2.0.1",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"priority-queue",
|
||||
@@ -3465,6 +3479,15 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "2.0.1"
|
||||
@@ -3475,6 +3498,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3127afbfc30b4cad60c34aeb741fb562a808642b81142bcf4afb73142da960"
|
||||
dependencies = [
|
||||
"base64 0.21.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.6.0"
|
||||
@@ -5082,6 +5115,18 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"thiserror",
|
||||
"time 0.3.24",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.10"
|
||||
@@ -7341,10 +7386,12 @@ dependencies = [
|
||||
"gcp_auth",
|
||||
"git-version",
|
||||
"itertools 0.11.0",
|
||||
"jsonwebtoken",
|
||||
"lazy_static",
|
||||
"mysql_async",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"pem 3.0.1",
|
||||
"postgres-native-tls",
|
||||
"prometheus",
|
||||
"rand 0.8.5",
|
||||
@@ -7353,6 +7400,7 @@ dependencies = [
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.7",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tokio-postgres",
|
||||
|
||||
+3
-1
@@ -184,4 +184,6 @@ postgres-native-tls = "^0"
|
||||
native-tls = "^0"
|
||||
samael = { version = "0.0.12", features = ["xmlsec"] }
|
||||
gcp_auth = "0.9.0"
|
||||
rust_decimal = {version = "1.31.0", features = ["db-postgres"]}
|
||||
rust_decimal = {version = "1.31.0", features = ["db-postgres"]}
|
||||
jsonwebtoken = "8.3.0"
|
||||
pem = "3.0.1"
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TYPE SCRIPT_LANG ADD VALUE IF NOT EXISTS 'snowflake';
|
||||
@@ -37,6 +37,16 @@ pub fn parse_bigquery_sig(code: &str) -> anyhow::Result<MainArgSignature> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_snowflake_sig(code: &str) -> anyhow::Result<MainArgSignature> {
|
||||
let parsed = parse_snowflake_file(&code)?;
|
||||
if let Some(x) = parsed {
|
||||
let args = x;
|
||||
Ok(MainArgSignature { star_args: false, star_kwargs: false, args })
|
||||
} else {
|
||||
Err(anyhow!("Error parsing sql".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref RE_CODE_PGSQL: Regex = Regex::new(r#"(?m)\$(\d+)(?:::(\w+))?"#).unwrap();
|
||||
|
||||
@@ -48,6 +58,8 @@ lazy_static::lazy_static! {
|
||||
// -- @name (type) = default
|
||||
static ref RE_ARG_BIGQUERY: Regex = Regex::new(r#"(?m)^-- @(\w+) \((\w+(?:\[\])?)\)(?: ?\= ?(.+))? *[\r\n$]"#).unwrap();
|
||||
|
||||
static ref RE_ARG_SNOWFLAKE: Regex = Regex::new(r#"(?m)^-- \? (\w+) \((\w+)\)(?: ?\= ?(.+))? *[\r\n$]"#).unwrap();
|
||||
|
||||
}
|
||||
|
||||
fn parse_mysql_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
@@ -158,6 +170,36 @@ fn parse_bigquery_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
Ok(Some(args))
|
||||
}
|
||||
|
||||
fn parse_snowflake_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
let mut args: Vec<Arg> = vec![];
|
||||
|
||||
for cap in RE_ARG_SNOWFLAKE.captures_iter(code) {
|
||||
let name = cap.get(1).map(|x| x.as_str().to_string()).unwrap();
|
||||
let typ = cap
|
||||
.get(2)
|
||||
.map(|x| x.as_str().to_string().to_lowercase())
|
||||
.unwrap();
|
||||
let default = cap.get(3).map(|x| x.as_str().to_string());
|
||||
let has_default = default.is_some();
|
||||
let parsed_typ = parse_snowflake_typ(typ.as_str());
|
||||
|
||||
let parsed_default = default.and_then(|x| match parsed_typ {
|
||||
Typ::Int => x.parse::<i64>().ok().map(|x| json!(x)),
|
||||
Typ::Float => x.parse::<f64>().ok().map(|x| json!(x)),
|
||||
_ => Some(json!(x)),
|
||||
});
|
||||
args.push(Arg {
|
||||
name,
|
||||
typ: parsed_typ,
|
||||
default: parsed_default,
|
||||
otyp: Some(typ),
|
||||
has_default,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Some(args))
|
||||
}
|
||||
|
||||
pub fn parse_mysql_typ(typ: &str) -> Typ {
|
||||
match typ {
|
||||
"varchar" | "char" | "binary" | "varbinary" | "blob" | "text" | "enum" | "set" => {
|
||||
@@ -207,6 +249,18 @@ pub fn parse_bigquery_typ(typ: &str) -> Typ {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_snowflake_typ(typ: &str) -> Typ {
|
||||
match typ {
|
||||
"varchar" => Typ::Str(None),
|
||||
"binary" => Typ::Bytes,
|
||||
"date" | "time" | "timestamp" => Typ::Datetime,
|
||||
"int" => Typ::Int,
|
||||
"float" => Typ::Float,
|
||||
"boolean" => Typ::Bool,
|
||||
_ => Typ::Str(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"collaborators": [
|
||||
"Ruben Fiszel <ruben@windmill.dev>"
|
||||
],
|
||||
"version": "1.134.2",
|
||||
"version": "1.138.1",
|
||||
"files": [
|
||||
"windmill_parser_wasm_bg.wasm",
|
||||
"windmill_parser_wasm.js",
|
||||
|
||||
@@ -35,6 +35,11 @@ export function parse_mysql(code: string): string;
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_bigquery(code: string): string;
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_snowflake(code: string): string;
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
@@ -47,6 +52,7 @@ export interface InitOutput {
|
||||
readonly parse_sql: (a: number, b: number, c: number) => void;
|
||||
readonly parse_mysql: (a: number, b: number, c: number) => void;
|
||||
readonly parse_bigquery: (a: number, b: number, c: number) => void;
|
||||
readonly parse_snowflake: (a: number, b: number, c: number) => void;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
|
||||
|
||||
@@ -358,6 +358,29 @@ export function parse_bigquery(code) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} code
|
||||
* @returns {string}
|
||||
*/
|
||||
export function parse_snowflake(code) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
wasm.parse_snowflake(retptr, ptr0, len0);
|
||||
var r0 = getInt32Memory0()[retptr / 4 + 0];
|
||||
var r1 = getInt32Memory0()[retptr / 4 + 1];
|
||||
deferred2_0 = r0;
|
||||
deferred2_1 = r1;
|
||||
return getStringFromWasm0(r0, r1);
|
||||
} finally {
|
||||
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(f, args) {
|
||||
try {
|
||||
return f.apply(this, args);
|
||||
@@ -400,6 +423,10 @@ async function __wbg_load(module, imports) {
|
||||
function __wbg_get_imports() {
|
||||
const imports = {};
|
||||
imports.wbg = {};
|
||||
imports.wbg.__wbg_eval_d972bbef37d2cd5a = function(arg0, arg1) {
|
||||
const ret = eval(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
|
||||
takeObject(arg0);
|
||||
};
|
||||
@@ -451,10 +478,6 @@ function __wbg_get_imports() {
|
||||
const ret = new Error(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_eval_bb7d5dc518fdea6d = function(arg0, arg1) {
|
||||
const ret = eval(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) {
|
||||
const ret = getObject(arg0) == getObject(arg1);
|
||||
return ret;
|
||||
|
||||
Binary file not shown.
@@ -8,6 +8,7 @@ export function parse_python(a: number, b: number, c: number): void;
|
||||
export function parse_sql(a: number, b: number, c: number): void;
|
||||
export function parse_mysql(a: number, b: number, c: number): void;
|
||||
export function parse_bigquery(a: number, b: number, c: number): void;
|
||||
export function parse_snowflake(a: number, b: number, c: number): void;
|
||||
export function __wbindgen_malloc(a: number, b: number): number;
|
||||
export function __wbindgen_realloc(a: number, b: number, c: number, d: number): number;
|
||||
export function __wbindgen_add_to_stack_pointer(a: number): number;
|
||||
|
||||
@@ -44,3 +44,8 @@ pub fn parse_mysql(code: &str) -> String {
|
||||
pub fn parse_bigquery(code: &str) -> String {
|
||||
wrap_sig(windmill_parser_sql::parse_bigquery_sig(code))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_snowflake(code: &str) -> String {
|
||||
wrap_sig(windmill_parser_sql::parse_snowflake_sig(code))
|
||||
}
|
||||
|
||||
@@ -5672,18 +5672,7 @@ components:
|
||||
language:
|
||||
type: string
|
||||
enum:
|
||||
[
|
||||
python3,
|
||||
deno,
|
||||
go,
|
||||
bash,
|
||||
postgresql,
|
||||
mysql,
|
||||
bigquery,
|
||||
graphql,
|
||||
nativets,
|
||||
bun,
|
||||
]
|
||||
[python3, deno, go, bash, postgresql, mysql, bigquery, snowflake, graphql, nativets, bun]
|
||||
kind:
|
||||
type: string
|
||||
enum: [script, failure, trigger, command, approval]
|
||||
@@ -5748,18 +5737,7 @@ components:
|
||||
language:
|
||||
type: string
|
||||
enum:
|
||||
[
|
||||
python3,
|
||||
deno,
|
||||
go,
|
||||
bash,
|
||||
postgresql,
|
||||
mysql,
|
||||
bigquery,
|
||||
graphql,
|
||||
nativets,
|
||||
bun,
|
||||
]
|
||||
[python3, deno, go, bash, postgresql, mysql, bigquery, snowflake, graphql, nativets, bun]
|
||||
kind:
|
||||
type: string
|
||||
enum: [script, failure, trigger, command, approval]
|
||||
@@ -5929,18 +5907,7 @@ components:
|
||||
language:
|
||||
type: string
|
||||
enum:
|
||||
[
|
||||
python3,
|
||||
deno,
|
||||
go,
|
||||
bash,
|
||||
postgresql,
|
||||
mysql,
|
||||
bigquery,
|
||||
graphql,
|
||||
nativets,
|
||||
bun,
|
||||
]
|
||||
[python3, deno, go, bash, postgresql, mysql, bigquery, snowflake, graphql, nativets, bun]
|
||||
email:
|
||||
type: string
|
||||
visible_to_owner:
|
||||
@@ -6030,18 +5997,7 @@ components:
|
||||
language:
|
||||
type: string
|
||||
enum:
|
||||
[
|
||||
python3,
|
||||
deno,
|
||||
go,
|
||||
bash,
|
||||
postgresql,
|
||||
mysql,
|
||||
bigquery,
|
||||
graphql,
|
||||
nativets,
|
||||
bun,
|
||||
]
|
||||
[python3, deno, go, bash, postgresql, mysql, bigquery, snowflake, graphql, nativets, bun]
|
||||
is_skipped:
|
||||
type: boolean
|
||||
email:
|
||||
@@ -6470,18 +6426,7 @@ components:
|
||||
language:
|
||||
type: string
|
||||
enum:
|
||||
[
|
||||
python3,
|
||||
deno,
|
||||
go,
|
||||
bash,
|
||||
postgresql,
|
||||
mysql,
|
||||
bigquery,
|
||||
graphql,
|
||||
nativets,
|
||||
bun,
|
||||
]
|
||||
[python3, deno, go, bash, postgresql, mysql, bigquery, snowflake, graphql, nativets, bun]
|
||||
tag:
|
||||
type: string
|
||||
kind:
|
||||
|
||||
@@ -1477,6 +1477,7 @@ async fn tarball_workspace(
|
||||
ScriptLang::Postgresql => "pg.sql",
|
||||
ScriptLang::Mysql => "my.sql",
|
||||
ScriptLang::Bigquery => "bq.sql",
|
||||
ScriptLang::Snowflake => "sf.sql",
|
||||
ScriptLang::Nativets => "fetch.ts",
|
||||
ScriptLang::Bun => "bun.ts",
|
||||
};
|
||||
|
||||
@@ -34,6 +34,7 @@ pub enum ScriptLang {
|
||||
Bun,
|
||||
Mysql,
|
||||
Bigquery,
|
||||
Snowflake,
|
||||
}
|
||||
|
||||
impl ScriptLang {
|
||||
@@ -48,6 +49,7 @@ impl ScriptLang {
|
||||
ScriptLang::Postgresql => "postgresql",
|
||||
ScriptLang::Mysql => "mysql",
|
||||
ScriptLang::Bigquery => "bigquery",
|
||||
ScriptLang::Snowflake => "snowflake",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ lazy_static::lazy_static! {
|
||||
"bun".to_string(),
|
||||
"postgresql".to_string(),
|
||||
"bigquery".to_string(),
|
||||
"snowflake".to_string(),
|
||||
"dependency".to_string(),
|
||||
"flow".to_string(),
|
||||
"hub".to_string(),
|
||||
|
||||
@@ -11,7 +11,7 @@ path = "src/lib.rs"
|
||||
[features]
|
||||
default = []
|
||||
deno-lock = []
|
||||
enterprise = ["windmill-queue/enterprise", "dep:gcp_auth"]
|
||||
enterprise = ["windmill-queue/enterprise", "dep:gcp_auth", "dep:jsonwebtoken", "dep:pem", "dep:sha2"]
|
||||
|
||||
[dependencies]
|
||||
windmill-queue.workspace = true
|
||||
@@ -65,6 +65,9 @@ mysql_async.workspace = true
|
||||
base64.workspace = true
|
||||
gcp_auth = { workspace = true, optional = true }
|
||||
rust_decimal.workspace = true
|
||||
jsonwebtoken = { workspace = true, optional = true }
|
||||
sha2 = { workspace = true, optional = true }
|
||||
pem = { workspace = true, optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
deno_fetch.workspace = true
|
||||
|
||||
@@ -122,7 +122,7 @@ pub async fn do_bigquery(
|
||||
.unwrap_or(&vec![])
|
||||
.iter()
|
||||
.map(|x| {
|
||||
convert_val(base_type.to_string(), x.clone()).ok().unwrap()
|
||||
convert_val(base_type.to_string(), x.clone())
|
||||
})
|
||||
.collect::<Vec<Value>>()
|
||||
}
|
||||
@@ -134,7 +134,7 @@ pub async fn do_bigquery(
|
||||
"type": arg_t.to_uppercase()
|
||||
},
|
||||
"parameterValue": {
|
||||
"value": convert_val(arg_t, arg_v)?,
|
||||
"value": convert_val(arg_t, arg_v),
|
||||
}
|
||||
})
|
||||
};
|
||||
@@ -239,7 +239,7 @@ pub async fn do_bigquery(
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_val(arg_t: String, arg_v: Value) -> Result<Value, Error> {
|
||||
fn convert_val(arg_t: String, arg_v: Value) -> Value {
|
||||
match arg_t.as_str() {
|
||||
"timestamp" | "datetime" | "date" | "time" => {
|
||||
let mut v: String = arg_v.as_str().unwrap_or("").to_owned();
|
||||
@@ -269,7 +269,7 @@ fn convert_val(arg_t: String, arg_v: Value) -> Result<Value, Error> {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(json!({ "value": json!(v) }))
|
||||
json!({ "value": json!(v) })
|
||||
}
|
||||
_ => {
|
||||
let mut v = arg_v;
|
||||
@@ -279,10 +279,10 @@ fn convert_val(arg_t: String, arg_v: Value) -> Result<Value, Error> {
|
||||
v = json!(v.to_string());
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
json!({
|
||||
"value": v,
|
||||
}
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#[cfg(feature = "enterprise")]
|
||||
mod bigquery_executor;
|
||||
#[cfg(feature = "enterprise")]
|
||||
mod snowflake_executor;
|
||||
|
||||
mod common;
|
||||
mod global_cache;
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
use base64::{engine, Engine as _};
|
||||
use core::fmt::Write;
|
||||
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
|
||||
use pem;
|
||||
use serde_json::{json, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use windmill_common::error::Error;
|
||||
use windmill_common::jobs::QueuedJob;
|
||||
use windmill_parser_sql::parse_snowflake_sig;
|
||||
use windmill_queue::HTTP_CLIENT;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{get_content, transform_json_value, AuthedClient, JobCompleted};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Claims {
|
||||
iss: String,
|
||||
sub: String,
|
||||
iat: i64,
|
||||
exp: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SnowflakeDatabase {
|
||||
account_identifier: String,
|
||||
public_key: String,
|
||||
private_key: String,
|
||||
username: String,
|
||||
database: Option<String>,
|
||||
schema: Option<String>,
|
||||
warehouse: Option<String>,
|
||||
role: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct SnowflakeResponse {
|
||||
data: Vec<Vec<Value>>,
|
||||
resultSetMetaData: SnowflakeResultSetMetaData,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct SnowflakeResultSetMetaData {
|
||||
numRows: i64,
|
||||
rowType: Vec<SnowflakeRowType>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SnowflakeRowType {
|
||||
name: String,
|
||||
r#type: String,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Deserialize)]
|
||||
struct SnowflakeError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
pub async fn do_snowflake(
|
||||
job: QueuedJob,
|
||||
client: &AuthedClient,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
) -> windmill_common::error::Result<JobCompleted> {
|
||||
let args = if let Some(args) = &job.args {
|
||||
Some(transform_json_value("args", client, &job.workspace_id, args.clone()).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let snowflake_args: Value = serde_json::from_value(args.unwrap_or_else(|| json!({})))
|
||||
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
|
||||
|
||||
let database = serde_json::from_value::<SnowflakeDatabase>(
|
||||
snowflake_args.get("database").unwrap_or(&json!({})).clone(),
|
||||
)
|
||||
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
|
||||
|
||||
let qualified_username =
|
||||
format!("{}.{}", database.account_identifier, database.username).to_uppercase();
|
||||
|
||||
let public_key = pem::parse(database.public_key.as_bytes()).map_err(|e| {
|
||||
Error::ExecutionErr(format!("Failed to parse public key: {}", e.to_string()))
|
||||
})?;
|
||||
let mut public_key_hash = Sha256::new();
|
||||
public_key_hash.update(public_key.contents());
|
||||
|
||||
let public_key_fp = engine::general_purpose::STANDARD.encode(public_key_hash.finalize());
|
||||
|
||||
let iss = format!("{}.SHA256:{}", qualified_username, public_key_fp);
|
||||
|
||||
let claims = Claims {
|
||||
iss: iss,
|
||||
sub: qualified_username,
|
||||
iat: chrono::Utc::now().timestamp(),
|
||||
exp: (chrono::Utc::now() + chrono::Duration::hours(1)).timestamp(),
|
||||
};
|
||||
|
||||
let private_key = EncodingKey::from_rsa_pem(database.private_key.as_bytes()).map_err(|e| {
|
||||
Error::ExecutionErr(format!("Failed to parse private key: {}", e.to_string()))
|
||||
})?;
|
||||
|
||||
let token = encode(&Header::new(Algorithm::RS256), &claims, &private_key)
|
||||
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
|
||||
|
||||
let args = &job
|
||||
.args
|
||||
.clone()
|
||||
.unwrap_or_else(|| json!({}))
|
||||
.as_object()
|
||||
.map(|x| x.to_owned())
|
||||
.unwrap_or_else(|| json!({}).as_object().unwrap().to_owned());
|
||||
|
||||
let query = get_content(&job, db).await?;
|
||||
|
||||
let mut bindings = serde_json::Map::new();
|
||||
let sig = parse_snowflake_sig(&query)
|
||||
.map_err(|x| Error::ExecutionErr(x.to_string()))?
|
||||
.args;
|
||||
|
||||
let mut i = 1;
|
||||
for arg in &sig {
|
||||
let arg_t = arg.otyp.clone().unwrap_or_else(|| "string".to_string());
|
||||
let arg_v = args.get(&arg.name).cloned().unwrap_or(json!(""));
|
||||
let snowflake_v = convert_typ_val(arg_t, arg_v);
|
||||
|
||||
bindings.insert(i.to_string(), snowflake_v);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let mut body = serde_json::Map::new();
|
||||
if database.schema.is_some() {
|
||||
body.insert(
|
||||
"schema".to_string(),
|
||||
json!(database.schema.unwrap().to_uppercase()),
|
||||
);
|
||||
}
|
||||
if database.warehouse.is_some() {
|
||||
body.insert(
|
||||
"warehouse".to_string(),
|
||||
json!(database.warehouse.unwrap().to_uppercase()),
|
||||
);
|
||||
}
|
||||
if database.role.is_some() {
|
||||
body.insert(
|
||||
"role".to_string(),
|
||||
json!(database.role.unwrap().to_uppercase()),
|
||||
);
|
||||
}
|
||||
if database.database.is_some() {
|
||||
body.insert(
|
||||
"database".to_string(),
|
||||
json!(database.database.unwrap().to_uppercase()),
|
||||
);
|
||||
}
|
||||
body.insert("statement".to_string(), json!(query));
|
||||
body.insert("timeout".to_string(), json!(10)); // in seconds
|
||||
|
||||
if i > 1 {
|
||||
body.insert("bindings".to_string(), json!(bindings));
|
||||
}
|
||||
|
||||
let response = HTTP_CLIENT
|
||||
.post(format!(
|
||||
"https://{}.snowflakecomputing.com/api/v2/statements/",
|
||||
database.account_identifier.to_uppercase()
|
||||
))
|
||||
.bearer_auth(token)
|
||||
.header("X-Snowflake-Authorization-Token-Type", "KEYPAIR_JWT")
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
|
||||
|
||||
match response.error_for_status_ref() {
|
||||
Ok(_) => {
|
||||
let result = response
|
||||
.json::<SnowflakeResponse>()
|
||||
.await
|
||||
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
|
||||
|
||||
if result.resultSetMetaData.numRows > 10000 {
|
||||
return Err(Error::ExecutionErr(
|
||||
"More than 10000 rows were requested, use LIMIT 10000 to limit the number of rows".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let rows = result
|
||||
.data
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let mut row_map = serde_json::Map::new();
|
||||
row.iter()
|
||||
.zip(result.resultSetMetaData.rowType.iter())
|
||||
.for_each(|(val, row_type)| {
|
||||
row_map
|
||||
.insert(row_type.name.clone(), parse_val(&val, &row_type.r#type));
|
||||
});
|
||||
Value::from(row_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(JobCompleted { job: job, result: rows, logs: "".to_string(), success: true })
|
||||
}
|
||||
Err(e) => {
|
||||
let resp = response.text().await.unwrap_or("".to_string());
|
||||
match serde_json::from_str::<SnowflakeError>(&resp) {
|
||||
Ok(sf_err) => Err(Error::ExecutionErr(sf_err.message)),
|
||||
Err(_) => Err(Error::ExecutionErr(e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_typ_val(arg_t: String, arg_v: Value) -> Value {
|
||||
match arg_t.as_str() {
|
||||
"date" => {
|
||||
let arr = arg_v
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.split("T")
|
||||
.collect::<Vec<&str>>();
|
||||
json!({
|
||||
"type": "TEXT",
|
||||
"value": match arr.as_slice() {
|
||||
[date, _] => {
|
||||
json!(date)
|
||||
}
|
||||
_ => {
|
||||
arg_v
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
"time" => {
|
||||
let arr = arg_v
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.split("T")
|
||||
.collect::<Vec<&str>>();
|
||||
json!({
|
||||
"type": "TEXT",
|
||||
"value": match arr.as_slice() {
|
||||
[_, time] => {
|
||||
json!(time)
|
||||
}
|
||||
_ => {
|
||||
arg_v
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
"binary" => {
|
||||
// convert base64 to hex as expected by snowflake
|
||||
let bytes = engine::general_purpose::STANDARD
|
||||
.decode(arg_v.as_str().unwrap_or(""))
|
||||
.unwrap_or(vec![]);
|
||||
let mut hex = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
write!(hex, "{:02X}", byte).unwrap_or(());
|
||||
}
|
||||
json!({
|
||||
"type": "TEXT",
|
||||
"value": hex
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
let mut v = arg_v;
|
||||
|
||||
if !v.is_string() {
|
||||
// if not string, convert to string for api request
|
||||
v = json!(v.to_string());
|
||||
}
|
||||
|
||||
json!({
|
||||
"type": "TEXT", // snowflake infer type from schema
|
||||
"value": v
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_val(value: &Value, typ: &str) -> Value {
|
||||
let str_value = value.as_str().unwrap_or("").to_string();
|
||||
match typ.to_lowercase().as_str() {
|
||||
"boolean" => json!(str_value.parse::<bool>().ok().unwrap_or(false)),
|
||||
"real" | "time" | "timestamp_ltz" | "timestamp_ntz" => {
|
||||
json!(str_value.parse::<f64>().ok().unwrap_or(0.0))
|
||||
}
|
||||
"fixed" | "date" | "number" => json!(str_value.parse::<i64>().ok().unwrap_or(0)),
|
||||
_ => value.clone(),
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ use crate::{
|
||||
};
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use crate::bigquery_executor::do_bigquery;
|
||||
use crate::{bigquery_executor::do_bigquery, snowflake_executor::do_snowflake};
|
||||
|
||||
pub async fn create_token_for_owner_in_bg(db: &Pool<Postgres>, job: &QueuedJob) -> Arc<RwLock<String>> {
|
||||
let rw_lock = Arc::new(RwLock::new(String::new()));
|
||||
@@ -351,6 +351,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
Some(ScriptLang::Postgresql),
|
||||
Some(ScriptLang::Mysql),
|
||||
Some(ScriptLang::Bigquery),
|
||||
Some(ScriptLang::Snowflake),
|
||||
Some(ScriptLang::Bun)];
|
||||
|
||||
let worker_execution_duration: HashMap<_, _> = all_langs.clone().into_iter().map(|x| (x.clone(), prometheus::register_histogram!(
|
||||
@@ -1336,6 +1337,17 @@ async fn handle_code_execution_job(
|
||||
let jc = do_bigquery(job.clone(), &client.get_authed().await, &db).await?;
|
||||
return Ok(jc.result)
|
||||
}
|
||||
} else if language == Some(ScriptLang::Snowflake) {
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
{
|
||||
return Err(Error::ExecutionErr("Snowflake is only available with an enterprise license".to_string()))
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
{
|
||||
let jc = do_snowflake(job.clone(), &client.get_authed().await, &db).await?;
|
||||
return Ok(jc.result)
|
||||
}
|
||||
} else if language == Some(ScriptLang::Nativets) {
|
||||
logs.push_str("\n--- FETCH TS EXECUTION ---\n");
|
||||
let jc = do_nativets(job.clone(), logs.clone(), &client.get_authed().await, inner_content).await?;
|
||||
@@ -2211,6 +2223,7 @@ async fn capture_dependency_job(
|
||||
ScriptLang::Postgresql => Ok("".to_owned()),
|
||||
ScriptLang::Mysql => Ok("".to_owned()),
|
||||
ScriptLang::Bigquery => Ok("".to_owned()),
|
||||
ScriptLang::Snowflake => Ok("".to_owned()),
|
||||
ScriptLang::Bash => Ok("".to_owned()),
|
||||
ScriptLang::Nativets => Ok("".to_owned()),
|
||||
|
||||
|
||||
+3
-1
@@ -59,8 +59,10 @@ async function dev(opts: GlobalOptions & { filter?: string }) {
|
||||
: ext == "sql"
|
||||
? splitted.length > 2 && splitted[splitted.length - 2] == "my"
|
||||
? "mysql"
|
||||
: splitted.length > 2 && splitted[splitted.length - 2] == "bigquery"
|
||||
: splitted.length > 2 && splitted[splitted.length - 2] == "bq"
|
||||
? "bigquery"
|
||||
: splitted.length > 2 && splitted[splitted.length - 2] == "sf"
|
||||
? "snowflake"
|
||||
: "postgresql"
|
||||
: "unknown";
|
||||
currentLastEdit = {
|
||||
|
||||
@@ -136,6 +136,7 @@ function ZipFSElement(zip: JSZip, useYaml: boolean): DynFSElement {
|
||||
else if (language == "postgresql") ext = "pg.sql";
|
||||
else if (language == "mysql") ext = "my.sql";
|
||||
else if (language == "bigquery") ext = "bq.sql";
|
||||
else if (language == "snowflake") ext = "sf.sql";
|
||||
else if (language == "bun") ext = "bun.ts";
|
||||
else if (language == "nativets") ext = "native.ts";
|
||||
|
||||
|
||||
Generated
+7
-7
@@ -41,7 +41,7 @@
|
||||
"svelte-timezone-picker": "^2.0.3",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"vscode-ws-jsonrpc": "3.0.0",
|
||||
"windmill-parser-wasm": "^1.134.2",
|
||||
"windmill-parser-wasm": "^1.138.1",
|
||||
"y-monaco": "^0.1.4",
|
||||
"y-websocket": "^1.5.0",
|
||||
"yjs": "^13.6.7"
|
||||
@@ -9777,9 +9777,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/windmill-parser-wasm": {
|
||||
"version": "1.134.2",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.134.2.tgz",
|
||||
"integrity": "sha512-XxAPj/JDfQVmvJCJ+8rktlAOsesvznARTPcLsBEKvN8MvG5jt2V3Xqh66MvmDVKl6QvHqOWFVetpd5LVrCDBSQ=="
|
||||
"version": "1.138.1",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.138.1.tgz",
|
||||
"integrity": "sha512-KGbiKKk7i8xRV+kcGXNRChIVXRQRAQ6eAGVJo7c2qjq+Sft3cGteWbkgeh8gjU5+KdmnP866jGwsRtw+zW2+5A=="
|
||||
},
|
||||
"node_modules/wordwrap": {
|
||||
"version": "1.0.0",
|
||||
@@ -16766,9 +16766,9 @@
|
||||
}
|
||||
},
|
||||
"windmill-parser-wasm": {
|
||||
"version": "1.134.2",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.134.2.tgz",
|
||||
"integrity": "sha512-XxAPj/JDfQVmvJCJ+8rktlAOsesvznARTPcLsBEKvN8MvG5jt2V3Xqh66MvmDVKl6QvHqOWFVetpd5LVrCDBSQ=="
|
||||
"version": "1.138.1",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.138.1.tgz",
|
||||
"integrity": "sha512-KGbiKKk7i8xRV+kcGXNRChIVXRQRAQ6eAGVJo7c2qjq+Sft3cGteWbkgeh8gjU5+KdmnP866jGwsRtw+zW2+5A=="
|
||||
},
|
||||
"wordwrap": {
|
||||
"version": "1.0.0",
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
"svelte-timezone-picker": "^2.0.3",
|
||||
"tailwind-merge": "^1.13.2",
|
||||
"vscode-ws-jsonrpc": "3.0.0",
|
||||
"windmill-parser-wasm": "^1.134.2",
|
||||
"windmill-parser-wasm": "^1.138.1",
|
||||
"y-monaco": "^0.1.4",
|
||||
"y-websocket": "^1.5.0",
|
||||
"yjs": "^13.6.7"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { JobService, Preview } from '$lib/gen'
|
||||
import { dbSchema, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { onDestroy } from 'svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import Drawer from './common/drawer/Drawer.svelte'
|
||||
@@ -108,33 +107,52 @@ export async function main(args: any) {
|
||||
async function getSchema() {
|
||||
if (!resourceType || !resourcePath) return
|
||||
dbSchema.set(undefined)
|
||||
try {
|
||||
const job = await JobService.runScriptPreview({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
language: 'deno' as Preview.language,
|
||||
content: content[resourceType],
|
||||
args: {
|
||||
args: '$res:' + resourcePath
|
||||
}
|
||||
}
|
||||
})
|
||||
await new Promise((r) => setTimeout(r, 3000))
|
||||
const testResult = await JobService.getCompletedJob({
|
||||
workspace: $workspaceStore!,
|
||||
id: job
|
||||
})
|
||||
if (testResult) {
|
||||
if (!testResult.success) {
|
||||
throw new Error('Could not query DB schema')
|
||||
} else {
|
||||
dbSchema.set(testResult.result)
|
||||
|
||||
const job = await JobService.runScriptPreview({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
language: 'deno' as Preview.language,
|
||||
content: content[resourceType],
|
||||
args: {
|
||||
args: '$res:' + resourcePath
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
sendUserToast('Could not query DB schema', true)
|
||||
}
|
||||
})
|
||||
let i = 1
|
||||
const inter = setInterval(async () => {
|
||||
try {
|
||||
const testResult = await JobService.getCompletedJob({
|
||||
workspace: $workspaceStore!,
|
||||
id: job
|
||||
})
|
||||
if (testResult) {
|
||||
if (!testResult.success) {
|
||||
console.error(testResult.result?.['error']?.['message'])
|
||||
} else {
|
||||
dbSchema.set(testResult.result)
|
||||
}
|
||||
clearInterval(inter)
|
||||
}
|
||||
} catch (err) {
|
||||
if (i >= 5) {
|
||||
console.error('Could not query DB schema within 5s')
|
||||
clearInterval(inter)
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace: $workspaceStore!,
|
||||
id: job,
|
||||
requestBody: {
|
||||
reason: 'Could not query DB schema within 5s'
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
i += 1
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
$: resourcePath && resourceType && ['postgresql', 'mysql'].includes(resourceType) && getSchema()
|
||||
|
||||
@@ -94,54 +94,55 @@
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div class="flex flex-col w-full items-start">
|
||||
<div class="flex flex-row gap-x-1 w-full">
|
||||
<Select
|
||||
portal={!disablePortal}
|
||||
value={valueSelect}
|
||||
on:change={(e) => {
|
||||
value = e.detail.value
|
||||
valueSelect = e.detail
|
||||
}}
|
||||
on:clear={() => {
|
||||
value = undefined
|
||||
valueSelect = undefined
|
||||
}}
|
||||
items={collection}
|
||||
class="text-clip grow min-w-0"
|
||||
placeholder="{resourceType ?? 'any'} resource"
|
||||
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
|
||||
containerStyles={darkMode
|
||||
? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
|
||||
: SELECT_INPUT_DEFAULT_STYLE.containerStyles}
|
||||
/>
|
||||
|
||||
<div class="flex flex-row gap-x-1 w-full">
|
||||
<Select
|
||||
portal={!disablePortal}
|
||||
value={valueSelect}
|
||||
on:change={(e) => {
|
||||
value = e.detail.value
|
||||
valueSelect = e.detail
|
||||
}}
|
||||
on:clear={() => {
|
||||
value = undefined
|
||||
valueSelect = undefined
|
||||
}}
|
||||
items={collection}
|
||||
class="text-clip grow min-w-0"
|
||||
placeholder="{resourceType ?? 'any'} resource"
|
||||
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
|
||||
containerStyles={darkMode
|
||||
? SELECT_INPUT_DEFAULT_STYLE.containerStylesDark
|
||||
: SELECT_INPUT_DEFAULT_STYLE.containerStyles}
|
||||
/>
|
||||
{#if value && value != ''}
|
||||
<Button variant="border" size="xs" on:click={() => resourceEditor?.initEdit?.(value ?? '')}>
|
||||
<Icon scale={0.8} data={faPen} /></Button
|
||||
>
|
||||
{/if}
|
||||
|
||||
{#if value && value != ''}
|
||||
<Button variant="border" size="xs" on:click={() => resourceEditor?.initEdit?.(value ?? '')}>
|
||||
<Icon scale={0.8} data={faPen} /></Button
|
||||
<Button
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
on:click={() => appConnect?.open?.(resourceType)}
|
||||
>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
on:click={() => appConnect?.open?.(resourceType)}
|
||||
>
|
||||
<Icon scale={0.8} data={faPlus} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
loadResources(resourceType)
|
||||
}}
|
||||
>
|
||||
<Icon scale={0.8} data={faRotateRight} />
|
||||
</Button>
|
||||
<Icon scale={0.8} data={faPlus} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
loadResources(resourceType)
|
||||
}}
|
||||
>
|
||||
<Icon scale={0.8} data={faRotateRight} />
|
||||
</Button>
|
||||
</div>
|
||||
<DbSchemaExplorer {resourceType} resourcePath={value} />
|
||||
</div>
|
||||
<DbSchemaExplorer {resourceType} resourcePath={value} />
|
||||
|
||||
<style>
|
||||
:global(.svelte-select-list) {
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
let editor: Editor | undefined = undefined
|
||||
let scriptEditor: ScriptEditor | undefined = undefined
|
||||
|
||||
const enterpriseLangs = ['bigquery']
|
||||
const enterpriseLangs = ['bigquery', 'snowflake']
|
||||
|
||||
loadWorkerGroups()
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
langs.push(['PostgreSQL', Script.language.POSTGRESQL])
|
||||
langs.push(['MySQL', Script.language.MYSQL])
|
||||
langs.push(['BigQuery', Script.language.BIGQUERY])
|
||||
langs.push(['Snowflake', Script.language.SNOWFLAKE])
|
||||
if (SCRIPT_SHOW_GO) {
|
||||
langs.push(['Go', Script.language.GO])
|
||||
}
|
||||
|
||||
@@ -9,33 +9,46 @@
|
||||
export let resource_type: string | undefined
|
||||
export let args: Record<string, any> | any = {}
|
||||
|
||||
const content = {
|
||||
postgresql: `import { Client } from 'https://deno.land/x/postgres@v0.17.0/mod.ts'
|
||||
export async function main(args: any) {
|
||||
const scripts: {
|
||||
[key: string]: {
|
||||
code: string
|
||||
lang: string
|
||||
}
|
||||
} = {
|
||||
postgresql: {
|
||||
code: `import { Client } from 'https://deno.land/x/postgres@v0.17.0/mod.ts'
|
||||
export async function main(database: any) {
|
||||
const u = new URL("postgres://")
|
||||
u.hash = ''
|
||||
u.search = '?sslmode=' + args.sslmode
|
||||
u.pathname = args.dbname
|
||||
u.host = args.host
|
||||
u.port = args.port
|
||||
u.password = args.password
|
||||
u.username = args.user
|
||||
u.search = '?sslmode=' + database.sslmode
|
||||
u.pathname = database.dbname
|
||||
u.host = database.host
|
||||
u.port = database.port
|
||||
u.password = database.password
|
||||
u.username = database.user
|
||||
const client = new Client(u.toString())
|
||||
await client.connect()
|
||||
return 'Connection successful'
|
||||
}`,
|
||||
mysql: `import { Client } from "https://deno.land/x/mysql@v2.11.0/mod.ts";
|
||||
export async function main(args: any) {
|
||||
lang: 'deno'
|
||||
},
|
||||
mysql: {
|
||||
code: `import { Client } from "https://deno.land/x/mysql@v2.11.0/mod.ts";
|
||||
export async function main(database: any) {
|
||||
const conn = await new Client().connect({
|
||||
hostname: args.host,
|
||||
port: args.port,
|
||||
username: args.user,
|
||||
db: args.database,
|
||||
password: args.password,
|
||||
hostname: database.host,
|
||||
port: database.port,
|
||||
username: database.user,
|
||||
db: database.database,
|
||||
password: database.password,
|
||||
});
|
||||
await conn.query("SELECT 1");
|
||||
return "Connection successful";
|
||||
}`
|
||||
}`,
|
||||
lang: 'deno'
|
||||
},
|
||||
snowflake: {
|
||||
code: `select 1`,
|
||||
lang: 'snowflake'
|
||||
}
|
||||
}
|
||||
|
||||
let loading = false
|
||||
@@ -43,13 +56,15 @@ export async function main(args: any) {
|
||||
if (!resource_type) return
|
||||
loading = true
|
||||
|
||||
const resourceScript = scripts[resource_type]
|
||||
|
||||
const job = await JobService.runScriptPreview({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
language: 'deno' as Preview.language,
|
||||
content: content[resource_type],
|
||||
language: resourceScript.lang as Preview.language,
|
||||
content: resourceScript.code,
|
||||
args: {
|
||||
args
|
||||
database: args
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -62,17 +77,28 @@ export async function main(args: any) {
|
||||
})
|
||||
if (testResult) {
|
||||
sendUserToast(
|
||||
testResult.success ? testResult.result : testResult.result?.['error']?.['message'],
|
||||
testResult.success ? 'Connection successful' : testResult.result?.['error']?.['message'],
|
||||
!testResult.success
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
sendUserToast('Connection did not resolve after 5s')
|
||||
sendUserToast('Connection did not resolve after 5s', true)
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace: $workspaceStore!,
|
||||
id: job,
|
||||
requestBody: {
|
||||
reason: 'Connection did not resolve after 5s'
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if resource_type == 'postgresql' || resource_type == 'mysql'}
|
||||
{#if Object.keys(scripts).includes(resource_type || '')}
|
||||
<Button
|
||||
spacingSize="sm"
|
||||
size="xs"
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
['postgresql', 'PostgreSQL'],
|
||||
['mysql', 'MySQL'],
|
||||
['bigquery', 'BigQuery'],
|
||||
['snowflake', 'Snowflake'],
|
||||
['bun', 'TypeScript (Bun)']
|
||||
] as [Script.language, string][]
|
||||
</script>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { Script } from '$lib/gen'
|
||||
import PowershellIcon from '$lib/components/icons/PowershellIcon.svelte'
|
||||
import BigQueryIcon from '$lib/components/icons/BigQueryIcon.svelte'
|
||||
import SnowflakeIcon from '$lib/components/icons/SnowflakeIcon.svelte'
|
||||
|
||||
export let lang:
|
||||
| SupportedLanguage
|
||||
@@ -48,6 +49,7 @@
|
||||
pgsql: PostgresIcon,
|
||||
mysql: MySQLIcon,
|
||||
bigquery: BigQueryIcon,
|
||||
snowflake: SnowflakeIcon,
|
||||
javascript: JavaScript,
|
||||
fetch: FetchIcon,
|
||||
docker: DockerIcon,
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<span class="font-bold text-xs">Identity (input copied to output)</span>
|
||||
{:else if flowModule?.value.type === 'rawscript'}
|
||||
<div class="w-8 mx-0.5">
|
||||
<LanguageIcon lang={flowModule.value.language} class="w-4 h-4" />
|
||||
<LanguageIcon lang={flowModule.value.language} width={20} height={20} />
|
||||
</div>
|
||||
<input bind:value={flowModule.summary} placeholder={'Summary'} class="w-full grow" />
|
||||
{:else if flowModule?.value.type === 'script' && 'path' in flowModule.value && flowModule.value.path}
|
||||
|
||||
@@ -176,6 +176,17 @@
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<FlowScriptPicker
|
||||
label="Snowflake"
|
||||
lang={Script.language.SNOWFLAKE}
|
||||
on:click={() => {
|
||||
dispatch('new', {
|
||||
language: RawScript.language.SNOWFLAKE,
|
||||
kind,
|
||||
subkind: 'flow'
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
<FlowScriptPicker
|
||||
label={`Docker`}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
export let icon: IconDefinition | undefined = undefined
|
||||
export let iconColor: string | undefined = undefined
|
||||
|
||||
const enterpriseLangs = ['bigquery']
|
||||
const enterpriseLangs = ['bigquery', 'snowflake']
|
||||
</script>
|
||||
|
||||
<Popover disablePopup={!enterpriseLangs.includes(lang || '') || !!$enterpriseLicense}>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
export let height = '24px'
|
||||
export let width = '24px'
|
||||
</script>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 146.36 139.16" {width} {height}
|
||||
><defs
|
||||
><style>
|
||||
.cls-1 {
|
||||
fill: #2bb5e9;
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
</style></defs
|
||||
><g
|
||||
><path
|
||||
class="cls-1"
|
||||
d="m134.81,60.1l-16.47,9.48,16.47,9.46c4.16,2.4,5.58,7.67,3.18,11.82-2.4,4.13-7.71,5.54-11.86,3.15l-29.5-16.96c-1.97-1.14-3.33-2.93-3.94-4.93-.29-.92-.42-1.88-.39-2.82.02-.68.12-1.36.31-2.04.59-2.11,1.97-4,4.02-5.19l29.5-16.94c4.15-2.38,9.46-.97,11.86,3.17,2.41,4.13.98,9.41-3.18,11.8Zm-15.6,45.96l-29.5-16.93c-1.58-.92-3.34-1.27-5.03-1.13-4.5.32-8.03,4.06-8.03,8.61v33.9c0,4.78,3.87,8.65,8.68,8.65s8.69-3.87,8.69-8.65v-18.96l16.52,9.49c4.15,2.4,9.46.98,11.85-3.15,2.39-4.14.98-9.44-3.18-11.83Zm-34.04-33.16l-12.26,12.19c-.35.36-1.03.65-1.54.65h-3.61c-.49,0-1.18-.29-1.54-.65l-12.26-12.19c-.35-.34-.63-1.03-.63-1.52v-3.59c0-.5.28-1.19.63-1.54l12.26-12.19c.36-.36,1.04-.64,1.54-.64h3.61c.5,0,1.19.28,1.54.64l12.26,12.19c.35.35.63,1.04.63,1.54v3.59c0,.49-.28,1.18-.63,1.52Zm-9.8-3.38c0-.49-.3-1.18-.65-1.54l-3.55-3.52c-.35-.35-1.04-.64-1.53-.64h-.14c-.49,0-1.18.29-1.53.64l-3.55,3.52c-.36.36-.63,1.05-.63,1.54v.14c0,.49.27,1.17.63,1.52l3.55,3.53c.35.35,1.03.64,1.53.64h.14c.49,0,1.18-.29,1.53-.64l3.55-3.53c.35-.35.65-1.03.65-1.52v-.14ZM19.93,33.08l29.5,16.95c1.58.91,3.34,1.27,5.04,1.13,4.49-.33,8.03-4.08,8.03-8.62V8.64c0-4.77-3.9-8.64-8.68-8.64s-8.69,3.87-8.69,8.64v18.97l-16.53-9.5c-4.14-2.39-9.45-.97-11.85,3.16-2.39,4.15-.98,9.43,3.17,11.82Zm64.76,18.08c1.69.14,3.45-.22,5.03-1.13l29.5-16.95c4.16-2.39,5.57-7.67,3.18-11.82-2.39-4.13-7.7-5.55-11.85-3.16l-16.52,9.5V8.64c0-4.77-3.88-8.64-8.69-8.64s-8.68,3.87-8.68,8.64v33.9c0,4.55,3.53,8.29,8.03,8.62Zm-30.22,36.84c-1.69-.14-3.46.21-5.04,1.13l-29.5,16.93c-4.15,2.39-5.57,7.69-3.17,11.83,2.41,4.13,7.71,5.55,11.85,3.15l16.53-9.49v18.96c0,4.78,3.88,8.65,8.69,8.65s8.68-3.87,8.68-8.65v-33.9c0-4.55-3.54-8.29-8.03-8.61Zm-8-15.88c.29-.92.42-1.88.39-2.82-.03-.68-.12-1.36-.32-2.04-.58-2.11-1.97-4-4.04-5.19l-29.48-16.94c-4.16-2.38-9.47-.97-11.86,3.17-2.41,4.13-.99,9.41,3.17,11.8l16.47,9.48-16.47,9.46c-4.16,2.4-5.58,7.67-3.17,11.82,2.39,4.13,7.69,5.54,11.86,3.15l29.48-16.96c1.99-1.14,3.34-2.93,3.96-4.93ZM139.62,15.91h-1.32v1.62h1.32c.61,0,1.01-.28,1.01-.8,0-.55-.37-.83-1.01-.83Zm-2.94-1.5h2.97c1.62,0,2.7.89,2.7,2.27,0,.86-.43,1.5-1.07,1.9l1.16,1.69v.34h-1.69l-1.13-1.62h-1.32v1.62h-1.62v-6.19Zm8.31,3.22c0-3.31-2.21-5.82-5.58-5.82s-5.52,2.39-5.52,5.82,2.21,5.82,5.52,5.82,5.58-2.51,5.58-5.82Zm1.38,0c0,3.89-2.61,7.14-6.96,7.14s-6.9-3.28-6.9-7.14,2.57-7.14,6.9-7.14,6.96,3.25,6.96,7.14Z"
|
||||
/></g
|
||||
></svg
|
||||
>
|
||||
@@ -10,7 +10,8 @@ import init, {
|
||||
parse_python,
|
||||
parse_sql,
|
||||
parse_mysql,
|
||||
parse_bigquery
|
||||
parse_bigquery,
|
||||
parse_snowflake
|
||||
} from 'windmill-parser-wasm'
|
||||
import wasmUrl from 'windmill-parser-wasm/windmill_parser_wasm_bg.wasm?url'
|
||||
import { workspaceStore } from './stores.js'
|
||||
@@ -56,6 +57,12 @@ export async function inferArgs(
|
||||
{ name: 'database', typ: { resource: 'gcp_service_account' } },
|
||||
...inferedSchema.args
|
||||
]
|
||||
} else if (language == 'snowflake') {
|
||||
inferedSchema = JSON.parse(parse_snowflake(code))
|
||||
inferedSchema.args = [
|
||||
{ name: 'database', typ: { resource: 'snowflake' } },
|
||||
...inferedSchema.args
|
||||
]
|
||||
} else if (language == 'go') {
|
||||
inferedSchema = JSON.parse(parse_go(code))
|
||||
} else if (language == 'bash') {
|
||||
|
||||
@@ -141,6 +141,11 @@ export const BIGQUERY_INIT_CODE = `-- @name1 (string) = default arg
|
||||
INSERT INTO \`demodb.demo\` VALUES (@name1, @name2)
|
||||
`
|
||||
|
||||
export const SNOWFLAKE_INIT_CODE = `-- ? name1 (varchar) = default arg
|
||||
-- ? name2 (int)
|
||||
INSERT INTO demo VALUES (?, ?)
|
||||
`
|
||||
|
||||
export const FETCH_INIT_CODE = `export async function main(
|
||||
url: string | undefined,
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' = 'GET',
|
||||
@@ -262,6 +267,9 @@ const ALL_INITIAL_CODE = [
|
||||
PYTHON_INIT_CODE_TRIGGER,
|
||||
DENO_INIT_CODE,
|
||||
POSTGRES_INIT_CODE,
|
||||
MYSQL_INIT_CODE,
|
||||
BIGQUERY_INIT_CODE,
|
||||
SNOWFLAKE_INIT_CODE,
|
||||
DENO_INIT_CODE_TRIGGER,
|
||||
DENO_INIT_CODE_CLEAR,
|
||||
PYTHON_INIT_CODE_CLEAR,
|
||||
@@ -334,6 +342,8 @@ export function initialCode(
|
||||
return MYSQL_INIT_CODE
|
||||
} else if (language == 'bigquery') {
|
||||
return BIGQUERY_INIT_CODE
|
||||
} else if (language == 'snowflake') {
|
||||
return SNOWFLAKE_INIT_CODE
|
||||
} else if (language == 'bun') {
|
||||
if (subkind === 'flow') {
|
||||
return BUN_INIT_CODE_CLEAR
|
||||
|
||||
@@ -18,6 +18,8 @@ export function scriptLangToEditorLang(lang: Script.language) {
|
||||
return 'sql'
|
||||
} else if (lang == 'bigquery') {
|
||||
return 'sql'
|
||||
} else if (lang == "snowflake") {
|
||||
return 'sql'
|
||||
} else if (lang == 'python3') {
|
||||
return 'python'
|
||||
} else if (lang == 'bash') {
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
<Badge color="blue">{job.job_kind}</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
{#if job.tag && !['deno', 'python3', 'flow', 'other', 'go', 'postgresql', 'mysql', 'bigquery', 'nativets', 'bash', 'other', 'dependency'].includes(job.tag)}
|
||||
{#if job.tag && !['deno', 'python3', 'flow', 'other', 'go', 'postgresql', 'mysql', 'bigquery', 'snowflake', 'nativets', 'bash', 'other', 'dependency'].includes(job.tag)}
|
||||
<div>
|
||||
<Badge color="indigo">Worker group: {job.tag}</Badge>
|
||||
</div>
|
||||
|
||||
@@ -195,6 +195,7 @@ components:
|
||||
- postgresql
|
||||
- mysql
|
||||
- bigquery
|
||||
- snowflake
|
||||
- graphql
|
||||
- nativets
|
||||
path:
|
||||
|
||||
Reference in New Issue
Block a user