diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 5d68ce437c..b76088fa89 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -434,6 +434,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "asynchronous-codec" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] + [[package]] name = "atoi" version = "1.0.0" @@ -1260,6 +1273,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "connection-string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "510ca239cf13b7f8d16a2b48f263de7b4f8c566f0af58d901031473c76afb1e3" + [[package]] name = "console" version = "0.15.7" @@ -2055,6 +2074,70 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + [[package]] name = "encoding_rs" version = "0.8.31" @@ -2064,6 +2147,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enumflags2" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -4334,6 +4437,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "pretty-hex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" + [[package]] name = "prettyplease" version = "0.1.25" @@ -6792,6 +6901,34 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiberius" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6e2bf3e4b5be181a2a2ceff4b9b12e2684010d436a6958bd564fbc8094d44d" +dependencies = [ + "async-trait", + "asynchronous-codec", + "byteorder", + "bytes", + "chrono", + "connection-string", + "encoding", + "enumflags2", + "futures-util", + "num-traits", + "once_cell", + "pin-project-lite", + "pretty-hex", + "rustls-native-certs", + "rustls-pemfile", + "thiserror", + "tokio-rustls 0.23.4", + "tokio-util", + "tracing", + "uuid 1.5.0", +] + [[package]] name = "tiger" version = "0.1.0" @@ -7049,6 +7186,7 @@ checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -8284,8 +8422,10 @@ dependencies = [ "serde_json", "sha2 0.10.8", "sqlx 0.7.2", + "tiberius", "tokio", "tokio-postgres", + "tokio-util", "tracing", "urlencoding", "uuid 1.5.0", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 7b2c98ba1c..448462ba16 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -203,3 +203,4 @@ tokenizers = "0.14.1" candle-core = "0.3.0" candle-transformers = "0.3.0" candle-nn = "0.3.0" +tiberius = { version = "0.12.2", default-features = false, features = ["rustls", "tds73", "chrono"] } \ No newline at end of file diff --git a/backend/migrations/20231109101743_add_mssql.down.sql b/backend/migrations/20231109101743_add_mssql.down.sql new file mode 100644 index 0000000000..d2f607c5b8 --- /dev/null +++ b/backend/migrations/20231109101743_add_mssql.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20231109101743_add_mssql.up.sql b/backend/migrations/20231109101743_add_mssql.up.sql new file mode 100644 index 0000000000..e933699070 --- /dev/null +++ b/backend/migrations/20231109101743_add_mssql.up.sql @@ -0,0 +1,4 @@ +-- Add up migration script here +ALTER TYPE SCRIPT_LANG ADD VALUE IF NOT EXISTS 'mssql'; +-- Add up migration script here +UPDATE config set config = '{"worker_tags": ["nativets", "postgresql", "mysql", "graphql", "snowflake", "bigquery", "mssql"]}'::jsonb where name = 'worker__native' and config = '{"worker_tags": ["nativets", "postgresql", "mysql", "graphql", "snowflake", "bigquery"]}'::jsonb ; \ No newline at end of file diff --git a/backend/parsers/windmill-parser-sql/src/lib.rs b/backend/parsers/windmill-parser-sql/src/lib.rs index 44f005a7db..9e24c33c0b 100644 --- a/backend/parsers/windmill-parser-sql/src/lib.rs +++ b/backend/parsers/windmill-parser-sql/src/lib.rs @@ -47,6 +47,16 @@ pub fn parse_snowflake_sig(code: &str) -> anyhow::Result { } } +pub fn parse_mssql_sig(code: &str) -> anyhow::Result { + let parsed = parse_mssql_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(); @@ -60,6 +70,9 @@ lazy_static::lazy_static! { static ref RE_ARG_SNOWFLAKE: Regex = Regex::new(r#"(?m)^-- \? (\w+) \((\w+)\)(?: ?\= ?(.+))? *[\r\n$]"#).unwrap(); + + static ref RE_ARG_MSSQL: Regex = Regex::new(r#"(?m)^-- @(?:P|p)\d+ (\w+) \((\w+)\)(?: ?\= ?(.+))? *[\r\n$]"#).unwrap(); + } fn parse_mysql_file(code: &str) -> anyhow::Result>> { @@ -200,6 +213,36 @@ fn parse_snowflake_file(code: &str) -> anyhow::Result>> { Ok(Some(args)) } +fn parse_mssql_file(code: &str) -> anyhow::Result>> { + let mut args: Vec = vec![]; + + for cap in RE_ARG_MSSQL.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_mssql_typ(typ.as_str()); + + let parsed_default = default.and_then(|x| match parsed_typ { + Typ::Int => x.parse::().ok().map(|x| json!(x)), + Typ::Float => x.parse::().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" => { @@ -269,6 +312,20 @@ pub fn parse_snowflake_typ(typ: &str) -> Typ { } } +pub fn parse_mssql_typ(typ: &str) -> Typ { + match typ { + "char" | "text" | "varchar" | "nchar" | "nvarchar" | "ntext" => Typ::Str(None), + "binary" | "varbinary" | "image" => Typ::Bytes, + "date" | "datetime2" | "datetime" | "datetimeoffset" | "smalldatetime" | "time" => { + Typ::Datetime + } + "bigint" | "int" | "tinyint" | "smallint" => Typ::Int, + "float" | "real" | "numeric" | "decimal" => Typ::Float, + "bit" => Typ::Bool, + _ => Typ::Str(None), + } +} + #[cfg(test)] mod tests { diff --git a/backend/parsers/windmill-parser-wasm/pkg/package.json b/backend/parsers/windmill-parser-wasm/pkg/package.json index b2ada6eca7..d48f357e2f 100644 --- a/backend/parsers/windmill-parser-wasm/pkg/package.json +++ b/backend/parsers/windmill-parser-wasm/pkg/package.json @@ -3,7 +3,7 @@ "collaborators": [ "Ruben Fiszel " ], - "version": "1.190.0", + "version": "1.202.1", "files": [ "windmill_parser_wasm_bg.wasm", "windmill_parser_wasm.js", diff --git a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.d.ts b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.d.ts index 5c8a98c76f..987dd381cc 100644 --- a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.d.ts +++ b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.d.ts @@ -54,6 +54,11 @@ export function parse_snowflake(code: string): string; * @param {string} code * @returns {string} */ +export function parse_mssql(code: string): string; +/** +* @param {string} code +* @returns {string} +*/ export function parse_graphql(code: string): string; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; @@ -70,6 +75,7 @@ export interface InitOutput { 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 parse_mssql: (a: number, b: number, c: number) => void; readonly parse_graphql: (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; diff --git a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.js b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.js index cc0c9b8310..b5949c2113 100644 --- a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.js +++ b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm.js @@ -97,15 +97,6 @@ function getInt32Memory0() { return cachedInt32Memory0; } -function addHeapObject(obj) { - if (heap_next === heap.length) heap.push(heap.length + 1); - const idx = heap_next; - heap_next = heap[idx]; - - heap[idx] = obj; - return idx; -} - const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; @@ -115,6 +106,15 @@ function getStringFromWasm0(ptr, len) { return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); } +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + let cachedFloat64Memory0 = null; function getFloat64Memory0() { @@ -427,6 +427,29 @@ export function parse_snowflake(code) { } } +/** +* @param {string} code +* @returns {string} +*/ +export function parse_mssql(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_mssql(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); + } +} + /** * @param {string} code * @returns {string} @@ -503,6 +526,14 @@ function __wbg_get_imports() { getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }; + imports.wbg.__wbg_eval_f742bccbdcf34b02 = function(arg0, arg1) { + const ret = eval(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_error_new = function(arg0, arg1) { + const ret = new Error(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; imports.wbg.__wbindgen_boolean_get = function(arg0) { const v = getObject(arg0); const ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2; @@ -520,14 +551,6 @@ function __wbg_get_imports() { const ret = getObject(arg0) === getObject(arg1); return ret; }; - imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) { - const ret = BigInt.asUintN(64, arg0); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_error_new = function(arg0, arg1) { - const ret = new Error(getStringFromWasm0(arg0, arg1)); - return addHeapObject(ret); - }; imports.wbg.__wbindgen_number_get = function(arg0, arg1) { const obj = getObject(arg1); const ret = typeof(obj) === 'number' ? obj : undefined; @@ -543,19 +566,19 @@ function __wbg_get_imports() { const ret = getObject(arg0) in getObject(arg1); return ret; }; - imports.wbg.__wbg_eval_d090ffdbb4b6658b = function(arg0, arg1) { - const ret = eval(getStringFromWasm0(arg0, arg1)); + imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) { + const ret = BigInt.asUintN(64, arg0); return addHeapObject(ret); }; imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) { const ret = getObject(arg0) == getObject(arg1); return ret; }; - imports.wbg.__wbg_get_44be0491f933a435 = function(arg0, arg1) { + imports.wbg.__wbg_get_4a9aa5157afeb382 = function(arg0, arg1) { const ret = getObject(arg0)[arg1 >>> 0]; return addHeapObject(ret); }; - imports.wbg.__wbg_length_fff51ee6522a1a18 = function(arg0) { + imports.wbg.__wbg_length_cace2e0b3ddc0502 = function(arg0) { const ret = getObject(arg0).length; return ret; }; @@ -563,76 +586,76 @@ function __wbg_get_imports() { const ret = typeof(getObject(arg0)) === 'function'; return ret; }; - imports.wbg.__wbg_next_526fc47e980da008 = function(arg0) { + imports.wbg.__wbg_next_15da6a3df9290720 = function(arg0) { const ret = getObject(arg0).next; return addHeapObject(ret); }; - imports.wbg.__wbg_next_ddb3312ca1c4e32a = function() { return handleError(function (arg0) { + imports.wbg.__wbg_next_1989a20442400aaa = function() { return handleError(function (arg0) { const ret = getObject(arg0).next(); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_done_5c1f01fb660d73b5 = function(arg0) { + imports.wbg.__wbg_done_bc26bf4ada718266 = function(arg0) { const ret = getObject(arg0).done; return ret; }; - imports.wbg.__wbg_value_1695675138684bd5 = function(arg0) { + imports.wbg.__wbg_value_0570714ff7d75f35 = function(arg0) { const ret = getObject(arg0).value; return addHeapObject(ret); }; - imports.wbg.__wbg_iterator_97f0c81209c6c35a = function() { + imports.wbg.__wbg_iterator_7ee1a391d310f8e4 = function() { const ret = Symbol.iterator; return addHeapObject(ret); }; - imports.wbg.__wbg_get_97b561fb56f034b5 = function() { return handleError(function (arg0, arg1) { + imports.wbg.__wbg_get_2aff440840bb6202 = function() { return handleError(function (arg0, arg1) { const ret = Reflect.get(getObject(arg0), getObject(arg1)); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_call_cb65541d95d71282 = function() { return handleError(function (arg0, arg1) { + imports.wbg.__wbg_call_669127b9d730c650 = function() { return handleError(function (arg0, arg1) { const ret = getObject(arg0).call(getObject(arg1)); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_isArray_4c24b343cb13cfb1 = function(arg0) { + imports.wbg.__wbg_isArray_38525be7442aa21e = function(arg0) { const ret = Array.isArray(getObject(arg0)); return ret; }; - imports.wbg.__wbg_instanceof_ArrayBuffer_39ac22089b74fddb = function(arg0) { + imports.wbg.__wbg_instanceof_ArrayBuffer_c7cc317e5c29cc0d = function(arg0) { let result; try { result = getObject(arg0) instanceof ArrayBuffer; - } catch { + } catch (_) { result = false; } const ret = result; return ret; }; - imports.wbg.__wbg_isSafeInteger_bb8e18dd21c97288 = function(arg0) { + imports.wbg.__wbg_isSafeInteger_c38b0a16d0c7cef7 = function(arg0) { const ret = Number.isSafeInteger(getObject(arg0)); return ret; }; - imports.wbg.__wbg_entries_e51f29c7bba0c054 = function(arg0) { + imports.wbg.__wbg_entries_6d727b73ee02b7ce = function(arg0) { const ret = Object.entries(getObject(arg0)); return addHeapObject(ret); }; - imports.wbg.__wbg_buffer_085ec1f694018c4f = function(arg0) { + imports.wbg.__wbg_buffer_344d9b41efe96da7 = function(arg0) { const ret = getObject(arg0).buffer; return addHeapObject(ret); }; - imports.wbg.__wbg_new_8125e318e6245eed = function(arg0) { + imports.wbg.__wbg_new_d8a000788389a31e = function(arg0) { const ret = new Uint8Array(getObject(arg0)); return addHeapObject(ret); }; - imports.wbg.__wbg_set_5cf90238115182c3 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_set_dcfd613a3420f908 = function(arg0, arg1, arg2) { getObject(arg0).set(getObject(arg1), arg2 >>> 0); }; - imports.wbg.__wbg_length_72e2208bbc0efc61 = function(arg0) { + imports.wbg.__wbg_length_a5587d6cd79ab197 = function(arg0) { const ret = getObject(arg0).length; return ret; }; - imports.wbg.__wbg_instanceof_Uint8Array_d8d9cb2b8e8ac1d4 = function(arg0) { + imports.wbg.__wbg_instanceof_Uint8Array_19e6f142a5e7e1e1 = function(arg0) { let result; try { result = getObject(arg0) instanceof Uint8Array; - } catch { + } catch (_) { result = false; } const ret = result; diff --git a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm index 3b14273e74..3b96f0069d 100644 Binary files a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm and b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm differ diff --git a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm.d.ts b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm.d.ts index f189da5ba4..6be861abef 100644 --- a/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm.d.ts +++ b/backend/parsers/windmill-parser-wasm/pkg/windmill_parser_wasm_bg.wasm.d.ts @@ -11,6 +11,7 @@ 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 parse_mssql(a: number, b: number, c: number): void; export function parse_graphql(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; diff --git a/backend/parsers/windmill-parser-wasm/src/lib.rs b/backend/parsers/windmill-parser-wasm/src/lib.rs index 29966b994b..e4755cff7a 100644 --- a/backend/parsers/windmill-parser-wasm/src/lib.rs +++ b/backend/parsers/windmill-parser-wasm/src/lib.rs @@ -67,6 +67,11 @@ pub fn parse_snowflake(code: &str) -> String { wrap_sig(windmill_parser_sql::parse_snowflake_sig(code)) } +#[wasm_bindgen] +pub fn parse_mssql(code: &str) -> String { + wrap_sig(windmill_parser_sql::parse_mssql_sig(code)) +} + #[wasm_bindgen] pub fn parse_graphql(code: &str) -> String { wrap_sig(windmill_parser_graphql::parse_graphql_sig(code)) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index e351267034..2510be67c7 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -6587,6 +6587,7 @@ components: mysql, bigquery, snowflake, + mssql, graphql, nativets, bun, @@ -6668,6 +6669,7 @@ components: mysql, bigquery, snowflake, + mssql, graphql, nativets, bun, @@ -6857,6 +6859,7 @@ components: mysql, bigquery, snowflake, + mssql, graphql, nativets, bun, @@ -6964,6 +6967,7 @@ components: mysql, bigquery, snowflake, + mssql, graphql, nativets, bun, @@ -7478,6 +7482,7 @@ components: mysql, bigquery, snowflake, + mssql, graphql, nativets, bun, diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index 1b636e5010..2ae4d106b9 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -1633,6 +1633,7 @@ async fn tarball_workspace( ScriptLang::Mysql => "my.sql", ScriptLang::Bigquery => "bq.sql", ScriptLang::Snowflake => "sf.sql", + ScriptLang::Mssql => "ms.sql", ScriptLang::Graphql => "gql", ScriptLang::Nativets => "fetch.ts", ScriptLang::Bun => "bun.ts", diff --git a/backend/windmill-common/src/scripts.rs b/backend/windmill-common/src/scripts.rs index 853035ce65..62cfc5b116 100644 --- a/backend/windmill-common/src/scripts.rs +++ b/backend/windmill-common/src/scripts.rs @@ -42,6 +42,7 @@ pub enum ScriptLang { Bigquery, Snowflake, Graphql, + Mssql, } impl ScriptLang { @@ -58,6 +59,7 @@ impl ScriptLang { ScriptLang::Mysql => "mysql", ScriptLang::Bigquery => "bigquery", ScriptLang::Snowflake => "snowflake", + ScriptLang::Mssql => "mssql", ScriptLang::Graphql => "graphql", } } diff --git a/backend/windmill-common/src/worker.rs b/backend/windmill-common/src/worker.rs index 55cc46e1fe..3585f8f9b0 100644 --- a/backend/windmill-common/src/worker.rs +++ b/backend/windmill-common/src/worker.rs @@ -28,6 +28,7 @@ lazy_static::lazy_static! { "postgresql".to_string(), "bigquery".to_string(), "snowflake".to_string(), + "mssql".to_string(), "graphql".to_string(), "dependency".to_string(), "flow".to_string(), diff --git a/backend/windmill-worker/Cargo.toml b/backend/windmill-worker/Cargo.toml index 8eafe92b46..c67c9cecbd 100644 --- a/backend/windmill-worker/Cargo.toml +++ b/backend/windmill-worker/Cargo.toml @@ -10,7 +10,7 @@ path = "src/lib.rs" [features] default = [] -enterprise = ["windmill-queue/enterprise", "dep:gcp_auth", "dep:jsonwebtoken", "dep:pem", "dep:sha2"] +enterprise = ["windmill-queue/enterprise", "dep:gcp_auth", "dep:jsonwebtoken", "dep:pem", "dep:sha2", "dep:tiberius", "dep:tokio-util"] benchmark = ["windmill-queue/benchmark"] flamegraph = [] @@ -74,6 +74,8 @@ nix.workspace = true bytes.workspace = true reqwest.workspace = true hex.workspace = true +tiberius = { workspace = true, optional = true } +tokio-util = { workspace = true, optional = true } [build-dependencies] deno_fetch.workspace = true diff --git a/backend/windmill-worker/src/lib.rs b/backend/windmill-worker/src/lib.rs index 3857dd196a..dcbbe5fe67 100644 --- a/backend/windmill-worker/src/lib.rs +++ b/backend/windmill-worker/src/lib.rs @@ -1,6 +1,8 @@ #[cfg(feature = "enterprise")] mod bigquery_executor; #[cfg(feature = "enterprise")] +mod mssql_executor; +#[cfg(feature = "enterprise")] mod snowflake_executor; mod bash_executor; diff --git a/backend/windmill-worker/src/mssql_executor.rs b/backend/windmill-worker/src/mssql_executor.rs new file mode 100644 index 0000000000..0166138b49 --- /dev/null +++ b/backend/windmill-worker/src/mssql_executor.rs @@ -0,0 +1,236 @@ +use base64::{engine::general_purpose, Engine as _}; +use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc}; +use serde::Deserialize; +use serde_json::value::RawValue; +use serde_json::{Map, Value}; +use tiberius::{AuthMethod, Client, ColumnData, Config, FromSqlOwned, Query, Row}; +use tokio::net::TcpStream; +use tokio_util::compat::TokioAsyncWriteCompatExt; +use uuid::Uuid; +use windmill_common::error::{self, Error}; +use windmill_common::worker::to_raw_value; +use windmill_common::{error::to_anyhow, jobs::QueuedJob}; +use windmill_parser_sql::parse_mssql_sig; + +use crate::common::build_args_values; +use crate::AuthedClientBackgroundTask; + +#[derive(Deserialize)] +struct MssqlDatabase { + host: String, + user: String, + password: String, + port: Option, + dbname: String, +} + +pub async fn do_mssql( + job: &QueuedJob, + client: &AuthedClientBackgroundTask, + query: &str, + db: &sqlx::Pool, +) -> error::Result> { + let mssql_args = build_args_values(job, client, db).await?; + + let database = if let Some(db) = mssql_args.get("database") { + serde_json::from_value::(db.clone()) + .map_err(|e| Error::ExecutionErr(e.to_string()))? + } else { + return Err(Error::BadRequest("Missing database argument".to_string())); + }; + + let mut config = Config::new(); + + config.host(database.host); + config.port(database.port.unwrap_or(1433)); + config.database(database.dbname); + + // Using SQL Server authentication. + config.authentication(AuthMethod::sql_server(database.user, database.password)); + config.trust_cert(); // on production, it is not a good idea to do this + + let tcp = TcpStream::connect(config.get_addr()).await?; + tcp.set_nodelay(true)?; + + // To be able to use Tokio's tcp, we're using the `compat_write` from + // the `TokioAsyncWriteCompatExt` to get a stream compatible with the + // traits from the `futures` crate. + let mut client = Client::connect(config, tcp.compat_write()) + .await + .map_err(to_anyhow)?; + + let sig = parse_mssql_sig(&query) + .map_err(|x| Error::ExecutionErr(x.to_string()))? + .args; + + let mut prepared_query = Query::new(query.to_owned()); + for arg in &sig { + let arg_t = arg.otyp.clone().unwrap_or_else(|| "string".to_string()); + let arg_v = mssql_args + .get(&arg.name) + .cloned() + .unwrap_or(serde_json::json!("")); + json_value_to_sql(&mut prepared_query, &arg_v, &arg_t)?; + } + + // A response to a query is a stream of data, that must be + // polled to the end before querying again. Using streams allows + // fetching data in an asynchronous manner, if needed. + let stream = prepared_query.query(&mut client).await.map_err(to_anyhow)?; + let rows = stream + .into_results() + .await + .map_err(to_anyhow)? + .into_iter() + .map(|rows| { + let result = rows + .into_iter() + .map(|row| row_to_json(row)) + .collect::>, Error>>(); + result + }) + .collect::>>, Error>>()?; + + return Ok(to_raw_value(&rows)); +} + +fn json_value_to_sql<'a>( + query: &mut Query, + value: &Value, + arg_t: &String, +) -> windmill_common::error::Result<()> { + match value { + Value::Null => { + query.bind(None::); + } + Value::Bool(b) => { + query.bind(b.to_owned()); + } + Value::Number(n) if n.is_u64() && arg_t == "tinyint" => { + query.bind(n.as_u64().unwrap().to_owned() as u8) + } + Value::Number(n) if n.is_i64() && arg_t == "smallint" => { + query.bind(n.as_i64().unwrap().to_owned() as i16) + } + Value::Number(n) if n.is_i64() && arg_t == "int" => { + query.bind(n.as_i64().unwrap().to_owned() as i32) + } + Value::Number(n) if n.is_i64() && arg_t == "bigint" => { + query.bind(n.as_i64().unwrap().to_owned() as i64) + } + Value::Number(n) if n.is_f64() && arg_t == "real" => { + query.bind(n.as_f64().unwrap().to_owned() as f32) + } + Value::Number(n) => query.bind(n.as_f64().unwrap().to_owned()), + Value::String(s) if arg_t == "uuid" => query.bind(Uuid::parse_str(s).map_err(to_anyhow)?), + Value::String(s) if arg_t == "binary" || arg_t == "varbinary" || arg_t == "image" => { + query.bind(general_purpose::STANDARD.decode(s).map_err(to_anyhow)?) + } + Value::String(s) if arg_t == "date" => { + let date = NaiveDate::parse_from_str(s, "%Y-%m-%dT%H:%M:%S.%3fZ").unwrap_or_default(); + query.bind(date) + } + Value::String(s) if arg_t == "time" => { + let time = NaiveTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S.%3fZ").unwrap_or_default(); + query.bind(time) + } + Value::String(s) + if arg_t == "datetime" || arg_t == "datetime2" || arg_t == "smalldatetime" => + { + let datetime = + NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S.%3fZ").unwrap_or_default(); + query.bind(datetime) + } + Value::String(s) if arg_t == "datetimeoffset" => { + let datetime = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S.%3fZ") + .unwrap_or_default() + .and_utc(); + query.bind(datetime) + } + Value::String(s) => query.bind(s.to_owned()), + _ => { + return Err(Error::ExecutionErr(format!( + "Unsupported type in query: {:?} and signature {arg_t:?}", + value + ))) + } + }; + Ok(()) +} + +fn row_to_json(row: Row) -> Result, Error> { + let cols = row + .columns() + .iter() + .map(|x| x.to_owned()) + .collect::>(); + let mut map = Map::new(); + for (col, val) in cols.iter().zip(row.into_iter()) { + map.insert(col.name().to_string(), sql_to_json_value(val)?); + } + Ok(map) +} + +fn value_or_null( + val: Option, + convert: impl Fn(T) -> Result, +) -> Result { + val.map_or(Ok(Value::Null), convert) +} + +fn sql_to_json_value(val: ColumnData) -> Result { + match val { + ColumnData::Bit(x) => value_or_null(x, |x| Ok(Value::Bool(x))), + ColumnData::U8(x) => value_or_null(x, |x| Ok(Value::Number(x.into()))), + ColumnData::I16(x) => value_or_null(x, |x| Ok(Value::Number(x.into()))), + ColumnData::I32(x) => value_or_null(x, |x| Ok(Value::Number(x.into()))), + ColumnData::I64(x) => value_or_null(x, |x| Ok(Value::Number(x.into()))), + ColumnData::String(x) => value_or_null(x, |x| Ok(Value::String(x.to_string()))), + ColumnData::Binary(x) => value_or_null(x, |x| { + Ok(Value::String(general_purpose::STANDARD.encode(x.as_ref()))) + }), + ColumnData::F32(x) => value_or_null(x, |x| { + Ok(Value::Number( + serde_json::Number::from_f64(x.into()) + .ok_or(anyhow::anyhow!("invalid json-float"))?, + )) + }), + ColumnData::F64(x) => value_or_null(x, |x| { + Ok(Value::Number( + serde_json::Number::from_f64(x).ok_or(anyhow::anyhow!("invalid json-float"))?, + )) + }), + ColumnData::Guid(x) => value_or_null(x, |x| Ok(Value::String(x.to_string()))), + ColumnData::Xml(x) => value_or_null(x, |x| Ok(Value::String(x.to_string()))), + ColumnData::Numeric(x) => value_or_null(x, |x| { + Ok(Value::Number( + serde_json::Number::from_f64(x.into()) + .ok_or(anyhow::anyhow!("invalid json-float"))?, + )) + }), + ColumnData::DateTime(x) => value_or_null( + NaiveDateTime::from_sql_owned(ColumnData::DateTime(x)).map_err(to_anyhow)?, + |x| Ok(Value::String(x.to_string())), + ), + ColumnData::DateTime2(x) => value_or_null( + NaiveDateTime::from_sql_owned(ColumnData::DateTime2(x)).map_err(to_anyhow)?, + |x| Ok(Value::String(x.to_string())), + ), + ColumnData::SmallDateTime(x) => value_or_null( + NaiveDateTime::from_sql_owned(ColumnData::SmallDateTime(x)).map_err(to_anyhow)?, + |x| Ok(Value::String(x.to_string())), + ), + ColumnData::Time(x) => value_or_null( + NaiveTime::from_sql_owned(ColumnData::Time(x)).map_err(to_anyhow)?, + |x| Ok(Value::String(x.to_string())), + ), + ColumnData::Date(x) => value_or_null( + NaiveDate::from_sql_owned(ColumnData::Date(x)).map_err(to_anyhow)?, + |x| Ok(Value::String(x.to_string())), + ), + ColumnData::DateTimeOffset(x) => value_or_null( + DateTime::::from_sql_owned(ColumnData::DateTimeOffset(x)).map_err(to_anyhow)?, + |x| Ok(Value::String(x.to_string())), + ), + } +} diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index bf249ab5e9..9b436a4896 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -90,7 +90,9 @@ use crate::{ }; #[cfg(feature = "enterprise")] -use crate::{bigquery_executor::do_bigquery, snowflake_executor::do_snowflake}; +use crate::{ + bigquery_executor::do_bigquery, mssql_executor::do_mssql, snowflake_executor::do_snowflake, +}; pub async fn create_token_for_owner_in_bg( db: &Pool, @@ -1863,9 +1865,9 @@ pub async fn process_completed_job Ok("".to_owned()), ScriptLang::Bigquery => Ok("".to_owned()), ScriptLang::Snowflake => Ok("".to_owned()), + ScriptLang::Mssql => Ok("".to_owned()), ScriptLang::Graphql => Ok("".to_owned()), ScriptLang::Bash => Ok("".to_owned()), ScriptLang::Powershell => Ok("".to_owned()), diff --git a/cli/dev.ts b/cli/dev.ts index 6855ea58ae..8afb623289 100644 --- a/cli/dev.ts +++ b/cli/dev.ts @@ -68,6 +68,8 @@ async function dev(opts: GlobalOptions & { filter?: string }) { ? "bigquery" : len > 2 && splitted[len - 2] == "sf" ? "snowflake" + : len > 2 && splitted[len - 2] == "ms" + ? "mssql" : "postgresql" : ext == "gql" ? "graphql" diff --git a/cli/script.ts b/cli/script.ts index f8d81bde8b..79d2f62f45 100644 --- a/cli/script.ts +++ b/cli/script.ts @@ -260,6 +260,7 @@ export function inferContentTypeFromFilePath( | "mysql" | "bigquery" | "snowflake" + | "mssql" | "graphql" { if (contentPath.endsWith(".py")) { return "python3"; @@ -277,6 +278,8 @@ export function inferContentTypeFromFilePath( return "bigquery"; } else if (contentPath.endsWith(".sf.sql")) { return "snowflake"; + } else if (contentPath.endsWith(".ms.sql")) { + return "mssql"; } else if (contentPath.endsWith(".pg.sql")) { return "postgresql"; } else if (contentPath.endsWith(".gql")) { diff --git a/cli/sync.ts b/cli/sync.ts index 3d91367300..eff9c00e96 100644 --- a/cli/sync.ts +++ b/cli/sync.ts @@ -139,6 +139,7 @@ function ZipFSElement(zip: JSZip, useYaml: boolean): DynFSElement { else if (language == "mysql") ext = "my.sql"; else if (language == "bigquery") ext = "bq.sql"; else if (language == "snowflake") ext = "sf.sql"; + else if (language == "mssql") ext = "ms.sql"; else if (language == "graphql") ext = "gql"; else if (language == "bun") ext = "bun.ts"; else if (language == "nativets") ext = "native.ts"; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 83c1be3271..d3c1ef9a90 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -45,7 +45,7 @@ "svelte-tiny-virtual-list": "^2.0.5", "tailwind-merge": "^1.13.2", "vscode-ws-jsonrpc": "3.0.0", - "windmill-parser-wasm": "^1.190.0", + "windmill-parser-wasm": "^1.202.1", "y-monaco": "^0.1.4", "y-websocket": "^1.5.0", "yjs": "^13.6.7" @@ -10056,9 +10056,9 @@ } }, "node_modules/windmill-parser-wasm": { - "version": "1.190.0", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.190.0.tgz", - "integrity": "sha512-E2VUFVfsL2pHyaElUM8pK4aoV3KKTY50e1UZY14oKnQRmtYYrjxYfFFlOoFVbPyT29AyWws7ZXEt6M1qb8MwUw==" + "version": "1.202.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.202.1.tgz", + "integrity": "sha512-And4nVsHkhDKJL5mtE5YL1csEwtkegBJnxFtdar9g42Oui/F5L4lU2v5RF4rIcLt8fdkN8/Y1Fn/4oN0TYPJtw==" }, "node_modules/wordwrap": { "version": "1.0.0", @@ -17215,9 +17215,9 @@ } }, "windmill-parser-wasm": { - "version": "1.190.0", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.190.0.tgz", - "integrity": "sha512-E2VUFVfsL2pHyaElUM8pK4aoV3KKTY50e1UZY14oKnQRmtYYrjxYfFFlOoFVbPyT29AyWws7ZXEt6M1qb8MwUw==" + "version": "1.202.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.202.1.tgz", + "integrity": "sha512-And4nVsHkhDKJL5mtE5YL1csEwtkegBJnxFtdar9g42Oui/F5L4lU2v5RF4rIcLt8fdkN8/Y1Fn/4oN0TYPJtw==" }, "wordwrap": { "version": "1.0.0", diff --git a/frontend/package.json b/frontend/package.json index 84ff5b90ee..8f39a6315b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -123,7 +123,7 @@ "svelte-tiny-virtual-list": "^2.0.5", "tailwind-merge": "^1.13.2", "vscode-ws-jsonrpc": "3.0.0", - "windmill-parser-wasm": "^1.190.0", + "windmill-parser-wasm": "^1.202.1", "y-monaco": "^0.1.4", "y-websocket": "^1.5.0", "yjs": "^13.6.7" diff --git a/frontend/src/lib/components/AppConnect.svelte b/frontend/src/lib/components/AppConnect.svelte index c36a17fa3e..dcc6d3e523 100644 --- a/frontend/src/lib/components/AppConnect.svelte +++ b/frontend/src/lib/components/AppConnect.svelte @@ -73,7 +73,14 @@ export let newPageOAuth = false - const nativeLanguagesCategory = ['postgresql', 'mysql', 'bigquery', 'snowflake', 'graphql'] + const nativeLanguagesCategory = [ + 'postgresql', + 'mysql', + 'bigquery', + 'snowflake', + 'mssql', + 'graphql' + ] let filter = '' let manual = false diff --git a/frontend/src/lib/components/DBSchemaExplorer.svelte b/frontend/src/lib/components/DBSchemaExplorer.svelte index 98ea190087..96debdfe67 100644 --- a/frontend/src/lib/components/DBSchemaExplorer.svelte +++ b/frontend/src/lib/components/DBSchemaExplorer.svelte @@ -166,6 +166,50 @@ GROUP BY table_name".replace('{dataset.id}', dataset.id) return schema }, argName: 'database' + }, + ms_sql_server: { + argName: 'database', + code: `select TABLE_SCHEMA, TABLE_NAME, DATA_TYPE, COLUMN_NAME, COLUMN_DEFAULT from information_schema.columns where table_schema != 'sys'`, + lang: 'mssql', + processingFn: (rows) => { + const schemas = rows[0].reduce((acc, a) => { + const table_schema = a.TABLE_SCHEMA + delete a.TABLE_SCHEMA + acc[table_schema] = acc[table_schema] || [] + acc[table_schema].push(a) + return acc + }, {}) + const data = {} + for (const key in schemas) { + data[key] = schemas[key].reduce((acc, a) => { + const table_name = a.TABLE_NAME + delete a.TABLE_NAME + acc[table_name] = acc[table_name] || {} + const p: { + type: string + required: boolean + default?: string + } = { + type: a.DATA_TYPE, + required: a.is_nullable === 'NO' + } + if (a.column_default) { + p.default = a.COLUMN_DEFAULT + } + acc[table_name][a.COLUMN_NAME] = p + return acc + }, {}) + } + return data + } + } + } + + function resourceTypeToLang(rt: string) { + if (rt === 'ms_sql_server') { + return 'mssql' + } else { + return rt } } @@ -200,9 +244,9 @@ GROUP BY table_name".replace('{dataset.id}', dataset.id) const schema = processingFn !== undefined ? processingFn(testResult.result) : testResult.result $dbSchemas[resourcePath] = { - lang: resourceType as SQLSchema['lang'], + lang: resourceTypeToLang(resourceType) as SQLSchema['lang'], schema, - publicOnly: !!schema.public || !!schema.PUBLIC + publicOnly: !!schema.public || !!schema.PUBLIC || !!schema.dbo } } else { if (typeof testResult.result !== 'object' || !('__schema' in testResult.result)) { @@ -247,7 +291,7 @@ GROUP BY table_name".replace('{dataset.id}', dataset.id) function formatSchema(dbSchema: DBSchema) { if (dbSchema.lang !== 'graphql' && dbSchema.publicOnly) { - return dbSchema.schema.public || dbSchema.schema.PUBLIC || dbSchema + return dbSchema.schema.public || dbSchema.schema.PUBLIC || dbSchema.schema.dbo || dbSchema } else if (dbSchema.lang === 'mysql' && Object.keys(dbSchema.schema).length === 1) { return dbSchema.schema[Object.keys(dbSchema.schema)[0]] } else { @@ -293,9 +337,9 @@ GROUP BY table_name".replace('{dataset.id}', dataset.id) Refresh - {#if dbSchema.lang !== 'graphql' && (dbSchema.schema?.public || dbSchema.schema?.PUBLIC)} + {#if dbSchema.lang !== 'graphql' && (dbSchema.schema?.public || dbSchema.schema?.PUBLIC || dbSchema.schema?.dbo)} - + {/if} diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index ee03ada182..89c770c6c0 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -77,7 +77,7 @@ let editor: Editor | undefined = undefined let scriptEditor: ScriptEditor | undefined = undefined - const enterpriseLangs = ['bigquery', 'snowflake'] + const enterpriseLangs = ['bigquery', 'snowflake', 'mssql'] loadWorkerGroups() @@ -107,6 +107,7 @@ langs.push(['MySQL', Script.language.MYSQL]) langs.push(['BigQuery', Script.language.BIGQUERY]) langs.push(['Snowflake', Script.language.SNOWFLAKE]) + langs.push(['MS SQL Server', Script.language.MSSQL]) langs.push(['GraphQL', Script.language.GRAPHQL]) langs.push(['PowerShell', Script.language.POWERSHELL]) diff --git a/frontend/src/lib/components/TestConnection.svelte b/frontend/src/lib/components/TestConnection.svelte index 5afd849dc5..1da0141d1f 100644 --- a/frontend/src/lib/components/TestConnection.svelte +++ b/frontend/src/lib/components/TestConnection.svelte @@ -38,6 +38,11 @@ lang: 'snowflake', argName: 'database' }, + ms_sql_server: { + code: `SELECT 1`, + lang: 'mssql', + argName: 'database' + }, graphql: { code: '{ __typename }', lang: 'graphql', diff --git a/frontend/src/lib/components/WorkspaceGroup.svelte b/frontend/src/lib/components/WorkspaceGroup.svelte index 915c459a67..5e3819d073 100644 --- a/frontend/src/lib/components/WorkspaceGroup.svelte +++ b/frontend/src/lib/components/WorkspaceGroup.svelte @@ -61,7 +61,15 @@ 'other', 'bun' ] - const nativeTags = ['nativets', 'postgresql', 'mysql', 'graphql', 'snowflake', 'bigquery'] + const nativeTags = [ + 'nativets', + 'postgresql', + 'mysql', + 'graphql', + 'snowflake', + 'mssql', + 'bigquery' + ] let newTag: string = '' $: selected = nconfig?.dedicated_worker != undefined ? 'dedicated' : 'normal' diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/EmptyInlineScript.svelte b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/EmptyInlineScript.svelte index dd2b3ddafb..57acbc20b7 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/EmptyInlineScript.svelte +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/EmptyInlineScript.svelte @@ -103,6 +103,7 @@ ['mysql', 'MySQL'], ['bigquery', 'BigQuery'], ['snowflake', 'Snowflake'], + ['mssql', 'MS SQL Server'], ['graphql', 'GraphQL'], ['bun', 'TypeScript (Bun)'] ] as [Script.language, string][] diff --git a/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte b/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte index bd305260d8..1fea588316 100644 --- a/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte +++ b/frontend/src/lib/components/common/languageIcons/LanguageIcon.svelte @@ -12,6 +12,7 @@ import BigQueryIcon from '$lib/components/icons/BigQueryIcon.svelte' import SnowflakeIcon from '$lib/components/icons/SnowflakeIcon.svelte' import GraphqlIcon from '$lib/components/icons/GraphqlIcon.svelte' + import MSSqlServerIcon from '$lib/components/icons/MSSqlServerIcon.svelte' export let lang: | SupportedLanguage @@ -36,7 +37,8 @@ [Script.language.GRAPHQL]: 'GraphQL', [Script.language.POSTGRESQL]: 'Postgresql', [Script.language.BIGQUERY]: 'BigQuery', - [Script.language.SNOWFLAKE]: 'Snowflake' + [Script.language.SNOWFLAKE]: 'Snowflake', + [Script.language.MSSQL]: 'MS SQL Server' } const langToComponent: Record< @@ -53,6 +55,7 @@ mysql: MySQLIcon, bigquery: BigQueryIcon, snowflake: SnowflakeIcon, + mssql: MSSqlServerIcon, javascript: JavaScript, fetch: FetchIcon, docker: DockerIcon, diff --git a/frontend/src/lib/components/copilot/ScriptGen.svelte b/frontend/src/lib/components/copilot/ScriptGen.svelte index 8735648749..04b616b5ae 100644 --- a/frontend/src/lib/components/copilot/ScriptGen.svelte +++ b/frontend/src/lib/components/copilot/ScriptGen.svelte @@ -308,7 +308,7 @@ /> - {#if ['postgresql', 'mysql', 'snowflake', 'bigquery', 'graphql'].includes(lang) && dbSchema?.lang === lang} + {#if ['postgresql', 'mysql', 'snowflake', 'bigquery', 'mssql', 'graphql'].includes(lang) && dbSchema?.lang === lang}

@@ -318,9 +318,14 @@ In order to better generate the script, we pass the selected schema to GPT-4.

- {#if dbSchema.lang !== 'graphql' && (dbSchema.schema?.public || dbSchema.schema?.PUBLIC)} + {#if dbSchema.lang !== 'graphql' && (dbSchema.schema?.public || dbSchema.schema?.PUBLIC || dbSchema.schema?.dbo)} - + {/if} diff --git a/frontend/src/lib/components/copilot/lib.ts b/frontend/src/lib/components/copilot/lib.ts index 660e9d9df5..5ac987193b 100644 --- a/frontend/src/lib/components/copilot/lib.ts +++ b/frontend/src/lib/components/copilot/lib.ts @@ -154,7 +154,7 @@ function addDBSChema(scriptOptions: CopilotOptions, prompt: string) { const { dbSchema, language } = scriptOptions if ( dbSchema && - ['postgresql', 'mysql', 'snowflake', 'bigquery', 'graphql'].includes(language) && // make sure we are using a SQL/query language + ['postgresql', 'mysql', 'snowflake', 'bigquery', 'mssql', 'graphql'].includes(language) && // make sure we are using a SQL/query language language === dbSchema.lang // make sure we are using the same language as the schema ) { const { schema, lang } = dbSchema @@ -188,7 +188,8 @@ function addDBSChema(scriptOptions: CopilotOptions, prompt: string) { let finalSchema: typeof smallerSchema | (typeof smallerSchema)['schemaKey'] = smallerSchema if (dbSchema.publicOnly) { - finalSchema = smallerSchema.public || smallerSchema.PUBLIC || smallerSchema + finalSchema = + smallerSchema.public || smallerSchema.PUBLIC || smallerSchema.dbo || smallerSchema } else if (lang === 'mysql' && Object.keys(smallerSchema).length === 1) { finalSchema = smallerSchema[Object.keys(smallerSchema)[0]] } diff --git a/frontend/src/lib/components/copilot/prompts/edit.yaml b/frontend/src/lib/components/copilot/prompts/edit.yaml index 1b0e37bf13..5fe8153212 100644 --- a/frontend/src/lib/components/copilot/prompts/edit.yaml +++ b/frontend/src/lib/components/copilot/prompts/edit.yaml @@ -89,7 +89,6 @@ prompts: ``` You can define arguments by adding comments before the statement like that: `-- @name1 ({type})` or `-- @name2 ({type}) = default` (one per row). They can then be obtained directly in the statement with `@name1`, `@name2`, etc.... - I get the following error: {error} My instructions: {description} snowflake: @@ -100,7 +99,16 @@ prompts: ``` Arguments can be obtained directly in the statement with ?. Name the parameters by adding comments before the statement like that: `-- ? name1 ({type})` or `-- ? name2 ({type}) = default` (one per row) - I get the following error: {error} + + My instructions: {description} + mssql: + prompt: |- + Here's my Microsoft SQL Server code: + ```sql + {code} + ``` + + Arguments can be obtained directly in the statement with @p1, @p2, etc.. Name the parameters by adding comments before the statement like that: `-- @p1 name1 ({type})` or `-- @p2 name2 ({type}) = default` (one per row) My instructions: {description} graphql: @@ -111,7 +119,6 @@ prompts: ``` Add the needed arguments as query parameters. - I get the following error: {error} My instructions: {description} powershell: @@ -122,7 +129,6 @@ prompts: ``` Arguments can be obtained by calling the param function on the first line like that: `param($ParamName1, $ParamName2 = "default value", [{type}]$ParamName3, ...)` - I get the following error: {error} My instructions: {description} nativets: diff --git a/frontend/src/lib/components/copilot/prompts/editPrompt.ts b/frontend/src/lib/components/copilot/prompts/editPrompt.ts index 29a879b001..d1f6eb82aa 100644 --- a/frontend/src/lib/components/copilot/prompts/editPrompt.ts +++ b/frontend/src/lib/components/copilot/prompts/editPrompt.ts @@ -20,16 +20,19 @@ export const EDIT_PROMPT = { "prompt": "Here's my MySQL code: \n```sql\n{code}\n```\n\nArguments can be obtained directly in the statement with ?. Name the parameters by adding comments before the statement like that: `-- ? name1 ({type})` or `-- ? name2 ({type}) = default` (one per row)\n\nMy instructions: {description}" }, "bigquery": { - "prompt": "Here's my BigQuery code: \n```sql\n{code}\n```\n\nYou can define arguments by adding comments before the statement like that: `-- @name1 ({type})` or `-- @name2 ({type}) = default` (one per row). They can then be obtained directly in the statement with `@name1`, `@name2`, etc....\nI get the following error: {error}\n\nMy instructions: {description}" + "prompt": "Here's my BigQuery code: \n```sql\n{code}\n```\n\nYou can define arguments by adding comments before the statement like that: `-- @name1 ({type})` or `-- @name2 ({type}) = default` (one per row). They can then be obtained directly in the statement with `@name1`, `@name2`, etc....\n\nMy instructions: {description}" }, "snowflake": { - "prompt": "Here's my snowflake code: \n```sql\n{code}\n```\n\nArguments can be obtained directly in the statement with ?. Name the parameters by adding comments before the statement like that: `-- ? name1 ({type})` or `-- ? name2 ({type}) = default` (one per row)\nI get the following error: {error}\n\nMy instructions: {description}" + "prompt": "Here's my snowflake code: \n```sql\n{code}\n```\n\nArguments can be obtained directly in the statement with ?. Name the parameters by adding comments before the statement like that: `-- ? name1 ({type})` or `-- ? name2 ({type}) = default` (one per row)\n\nMy instructions: {description}" + }, + "mssql": { + "prompt": "Here's my Microsoft SQL Server code: \n```sql\n{code}\n```\n\nArguments can be obtained directly in the statement with @p1, @p2, etc.. Name the parameters by adding comments before the statement like that: `-- @p1 name1 ({type})` or `-- @p2 name2 ({type}) = default` (one per row)\n\nMy instructions: {description}" }, "graphql": { - "prompt": "Here's my graphql code: \n```graphql\n{code}\n```\n\nAdd the needed arguments as query parameters.\nI get the following error: {error}\n\nMy instructions: {description}" + "prompt": "Here's my graphql code: \n```graphql\n{code}\n```\n\nAdd the needed arguments as query parameters.\n\nMy instructions: {description}" }, "powershell": { - "prompt": "Here's my powershell code: \n```powershell\n{code}\n```\n\nArguments can be obtained by calling the param function on the first line like that: `param($ParamName1, $ParamName2 = \"default value\", [{type}]$ParamName3, ...)`\nI get the following error: {error}\n\nMy instructions: {description}" + "prompt": "Here's my powershell code: \n```powershell\n{code}\n```\n\nArguments can be obtained by calling the param function on the first line like that: `param($ParamName1, $ParamName2 = \"default value\", [{type}]$ParamName3, ...)`\n\nMy instructions: {description}" }, "nativets": { "prompt": "Here's my TypeScript code: \n```typescript\n{code}\n```\n\nWe have to export a \"main\" function like this: \"export async function main(...)\" and specify the parameter types but do not call it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: \"{resource_type}Resource\".\nThe following resource types are available:\n\n{resourceTypes}\n\nOnly define the type for resources that are actually needed to achieve the function purpose. The resource type name has to be exactly as specified. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n\nMy instructions: {description}" diff --git a/frontend/src/lib/components/copilot/prompts/fix.yaml b/frontend/src/lib/components/copilot/prompts/fix.yaml index ee7a8de5c6..21d6c3a3ee 100644 --- a/frontend/src/lib/components/copilot/prompts/fix.yaml +++ b/frontend/src/lib/components/copilot/prompts/fix.yaml @@ -111,6 +111,17 @@ prompts: I get the following error: {error} Fix my code. + mssql: + prompt: |- + Here's my Microsoft SQL Server code: + ```sql + {code} + ``` + + Arguments can be obtained directly in the statement with @p1, @p2, etc.. Name the parameters by adding comments before the statement like that: `-- @p1 name1 ({type})` or `-- @p2 name2 ({type}) = default` (one per row) + + I get the following error: {error} + Fix my code. graphql: prompt: |- Here's my graphql code: diff --git a/frontend/src/lib/components/copilot/prompts/fixPrompt.ts b/frontend/src/lib/components/copilot/prompts/fixPrompt.ts index 033c73c749..0ac7fd8c84 100644 --- a/frontend/src/lib/components/copilot/prompts/fixPrompt.ts +++ b/frontend/src/lib/components/copilot/prompts/fixPrompt.ts @@ -25,6 +25,9 @@ export const FIX_PROMPT = { "snowflake": { "prompt": "Here's my snowflake code: \n```sql\n{code}\n```\n\nArguments can be obtained directly in the statement with ?. Name the parameters by adding comments before the statement like that: `-- ? name1 ({type})` or `-- ? name2 ({type}) = default` (one per row)\n\nI get the following error: {error}\nFix my code." }, + "mssql": { + "prompt": "Here's my Microsoft SQL Server code: \n```sql\n{code}\n```\n\nArguments can be obtained directly in the statement with @p1, @p2, etc.. Name the parameters by adding comments before the statement like that: `-- @p1 name1 ({type})` or `-- @p2 name2 ({type}) = default` (one per row)\n\nI get the following error: {error}\nFix my code." + }, "graphql": { "prompt": "Here's my graphql code: \n```graphql\n{code}\n```\n\nAdd the needed arguments as query parameters.\n\nI get the following error: {error}\nFix my code." }, diff --git a/frontend/src/lib/components/copilot/prompts/gen.yaml b/frontend/src/lib/components/copilot/prompts/gen.yaml index 3adad28454..49c7da8359 100644 --- a/frontend/src/lib/components/copilot/prompts/gen.yaml +++ b/frontend/src/lib/components/copilot/prompts/gen.yaml @@ -45,6 +45,9 @@ prompts: snowflake: prompt: |- Write SQL code for snowflake that should {description}. Arguments can be obtained directly in the statement with ?. Name the parameters by adding comments before the statement like that: `-- ? name1 ({type})` or `-- ? name2 ({type}) = default` (one per row) + mssql: + prompt: |- + Write SQL code for Microsoft SQL Server that should {description}. Arguments can be obtained directly in the statement with @p1, @p2, etc.. Name the parameters by adding comments before the statement like that: `-- @p1 name1 ({type})` or `-- @p2 name2 ({type}) = default` (one per row) graphql: prompt: |- Write a GraphQL query that should {description}. Add the needed arguments as query parameters. diff --git a/frontend/src/lib/components/copilot/prompts/genPrompt.ts b/frontend/src/lib/components/copilot/prompts/genPrompt.ts index e6738cb6ff..5af6d697d6 100644 --- a/frontend/src/lib/components/copilot/prompts/genPrompt.ts +++ b/frontend/src/lib/components/copilot/prompts/genPrompt.ts @@ -25,6 +25,9 @@ export const GEN_PROMPT = { "snowflake": { "prompt": "Write SQL code for snowflake that should {description}. Arguments can be obtained directly in the statement with ?. Name the parameters by adding comments before the statement like that: `-- ? name1 ({type})` or `-- ? name2 ({type}) = default` (one per row)" }, + "mssql": { + "prompt": "Write SQL code for Microsoft SQL Server that should {description}. Arguments can be obtained directly in the statement with @p1, @p2, etc.. Name the parameters by adding comments before the statement like that: `-- @p1 name1 ({type})` or `-- @p2 name2 ({type}) = default` (one per row)" + }, "graphql": { "prompt": "Write a GraphQL query that should {description}. Add the needed arguments as query parameters." }, diff --git a/frontend/src/lib/components/flows/content/FlowInputs.svelte b/frontend/src/lib/components/flows/content/FlowInputs.svelte index 05df2a0584..e4c24ed421 100644 --- a/frontend/src/lib/components/flows/content/FlowInputs.svelte +++ b/frontend/src/lib/components/flows/content/FlowInputs.svelte @@ -219,6 +219,20 @@ }} /> + { + dispatch('new', { + language: RawScript.language.MSSQL, + kind, + subkind: 'flow', + summary + }) + }} + /> + diff --git a/frontend/src/lib/components/icons/MSSqlServerIcon.svelte b/frontend/src/lib/components/icons/MSSqlServerIcon.svelte new file mode 100644 index 0000000000..24fdcda841 --- /dev/null +++ b/frontend/src/lib/components/icons/MSSqlServerIcon.svelte @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/lib/components/icons/index.ts b/frontend/src/lib/components/icons/index.ts index 58b5e3029b..1f62901e1c 100644 --- a/frontend/src/lib/components/icons/index.ts +++ b/frontend/src/lib/components/icons/index.ts @@ -65,6 +65,7 @@ import GraphqlIcon from './GraphqlIcon.svelte' import NocoDbIcon from './NocoDbIcon.svelte' import AzureIcon from './AzureIcon.svelte' import OktaIcon from './OktaIcon.svelte' +import MsSqlServerIcon from './MSSqlServerIcon.svelte' export const APP_TO_ICON_COMPONENT = { postgresql: PostgresIcon, @@ -90,6 +91,7 @@ export const APP_TO_ICON_COMPONENT = { mailchimp: MailchimpIcon, sendgrid: SendgridIcon, snowflake: SendflakeIcon, + ms_sql_server: MsSqlServerIcon, qrcode: QRCodeIcon, linkedin: LinkedinIcon, hubspot: HubspotIcon, diff --git a/frontend/src/lib/infer.ts b/frontend/src/lib/infer.ts index cea708b36a..70edfbaa3b 100644 --- a/frontend/src/lib/infer.ts +++ b/frontend/src/lib/infer.ts @@ -14,7 +14,8 @@ import init, { parse_snowflake, parse_graphql, parse_powershell, - parse_outputs + parse_outputs, + parse_mssql } from 'windmill-parser-wasm' import wasmUrl from 'windmill-parser-wasm/windmill_parser_wasm_bg.wasm?url' import { workspaceStore } from './stores.js' @@ -66,6 +67,12 @@ export async function inferArgs( { name: 'database', typ: { resource: 'snowflake' } }, ...inferedSchema.args ] + } else if (language == 'mssql') { + inferedSchema = JSON.parse(parse_mssql(code)) + inferedSchema.args = [ + { name: 'database', typ: { resource: 'ms_sql_server' } }, + ...inferedSchema.args + ] } else if (language == 'graphql') { inferedSchema = JSON.parse(parse_graphql(code)) inferedSchema.args = [{ name: 'api', typ: { resource: 'graphql' } }, ...inferedSchema.args] diff --git a/frontend/src/lib/script_helpers.ts b/frontend/src/lib/script_helpers.ts index fb7255f9dc..81c75e976c 100644 --- a/frontend/src/lib/script_helpers.ts +++ b/frontend/src/lib/script_helpers.ts @@ -139,7 +139,8 @@ export async function main(message: string, name: string) { export const POSTGRES_INIT_CODE = `-- $1 name1 = default arg -- $2 name2 -INSERT INTO demo VALUES (\$1::TEXT, \$2::INT) RETURNING * +-- $3 name3 +INSERT INTO demo VALUES (\$1::TEXT, \$2::INT, \$3::TEXT[]) RETURNING * ` export const MYSQL_INIT_CODE = `-- ? name1 (text) = default arg @@ -158,6 +159,11 @@ export const SNOWFLAKE_INIT_CODE = `-- ? name1 (varchar) = default arg INSERT INTO demo VALUES (?, ?) ` +export const MSSQL_INIT_CODE = `-- @p1 name1 (varchar) = default arg +-- @p2 name2 (int) +INSERT INTO demo VALUES (@p1, @p2) +` + export const GRAPHQL_INIT_CODE = `query($name4: String, $name2: Int, $name3: [String]) { demo(name1: $name1, name2: $name2, name3: $name3) { name1, @@ -291,6 +297,7 @@ const ALL_INITIAL_CODE = [ MYSQL_INIT_CODE, BIGQUERY_INIT_CODE, SNOWFLAKE_INIT_CODE, + MSSQL_INIT_CODE, GRAPHQL_INIT_CODE, DENO_INIT_CODE_TRIGGER, DENO_INIT_CODE_CLEAR, @@ -368,6 +375,8 @@ export function initialCode( return BIGQUERY_INIT_CODE } else if (language == 'snowflake') { return SNOWFLAKE_INIT_CODE + } else if (language == 'mssql') { + return MSSQL_INIT_CODE } else if (language == 'graphql') { return GRAPHQL_INIT_CODE } else if (language == 'bun') { diff --git a/frontend/src/lib/scripts.ts b/frontend/src/lib/scripts.ts index 0dc7f772d2..4f8b125aab 100644 --- a/frontend/src/lib/scripts.ts +++ b/frontend/src/lib/scripts.ts @@ -20,6 +20,8 @@ export function scriptLangToEditorLang(lang: Script.language) { return 'sql' } else if (lang == 'snowflake') { return 'sql' + } else if (lang == 'mssql') { + return 'sql' } else if (lang == 'python3') { return 'python' } else if (lang == 'bash') { diff --git a/frontend/src/lib/stores.ts b/frontend/src/lib/stores.ts index e3eeacb572..f097855917 100644 --- a/frontend/src/lib/stores.ts +++ b/frontend/src/lib/stores.ts @@ -78,7 +78,7 @@ type SQLBaseSchema = { } export interface SQLSchema { - lang: 'mysql' | 'bigquery' | 'postgresql' | 'snowflake' + lang: 'mysql' | 'bigquery' | 'postgresql' | 'snowflake' | 'mssql' schema: SQLBaseSchema publicOnly: boolean | undefined } @@ -93,4 +93,3 @@ export type DBSchema = SQLSchema | GraphqlSchema type DBSchemas = Partial> export const dbSchemas = writable({}) - diff --git a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte index b2cbcb76e9..edc8029996 100644 --- a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte @@ -396,7 +396,7 @@ priority: {job.priority}
{/if} - {#if job.tag && !['deno', 'python3', 'flow', 'other', 'go', 'postgresql', 'mysql', 'bigquery', 'snowflake', 'graphql', 'nativets', 'bash', 'powershell', 'other', 'dependency'].includes(job.tag)} + {#if job.tag && !['deno', 'python3', 'flow', 'other', 'go', 'postgresql', 'mysql', 'bigquery', 'snowflake', 'mssql', 'graphql', 'nativets', 'bash', 'powershell', 'other', 'dependency'].includes(job.tag)}
Tag: {job.tag}
diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 83e642fa1e..941daa37e4 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -214,6 +214,7 @@ components: - mysql - bigquery - snowflake + - mssql - graphql - nativets path: