mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 08:04:25 +00:00
Merge remote-tracking branch 'origin/main' into glm/fix/frontend/prevent-multiple-menu-opening
# Conflicts: # frontend/src/lib/components/apps/components/display/AppMenu.svelte
This commit is contained in:
@@ -13,10 +13,10 @@ on:
|
||||
jobs:
|
||||
check-membership:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/aider') && !contains(github.event.comment.user.login, '[bot]')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/aider') && !contains(github.event.comment.user.login, '[bot]')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/aider') && !contains(github.event.review.user.login, '[bot]')) ||
|
||||
(github.event_name == 'issues' && contains(github.event.issue.body, '/aider') && !contains(github.event.issue.user.login, '[bot]'))
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/ai') && !contains(github.event.comment.user.login, '[bot]')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/ai') && !contains(github.event.comment.user.login, '[bot]')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/ai') && !contains(github.event.review.user.login, '[bot]')) ||
|
||||
(github.event_name == 'issues' && contains(github.event.issue.body, '/ai') && !contains(github.event.issue.user.login, '[bot]'))
|
||||
runs-on: ubicloud-standard-2
|
||||
outputs:
|
||||
is_member: ${{ steps.check-membership.outputs.is_member }}
|
||||
@@ -82,4 +82,4 @@ jobs:
|
||||
- Bash(npm run generate-backend-client): Generate the backend client. You need this to run npm run check.
|
||||
- Bash(cargo check): Run the cargo check script. You should run this tool after making changes to the backend code.
|
||||
- Bash(curl https://sh.rustup.rs -sSf | sh): Install Rust. You need this to run cargo check."
|
||||
trigger_phrase: "/aider"
|
||||
trigger_phrase: "/ai"
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## [1.493.0](https://github.com/windmill-labs/windmill/compare/v1.492.1...v1.493.0) (2025-05-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add aws oidc support for instance s3 storage ([#5810](https://github.com/windmill-labs/windmill/issues/5810)) ([5b96bcc](https://github.com/windmill-labs/windmill/commit/5b96bccedd6e68fea631580dd49338301ad0305f))
|
||||
* duckdb sql lang support ([#5761](https://github.com/windmill-labs/windmill/issues/5761)) ([fdefd4b](https://github.com/windmill-labs/windmill/commit/fdefd4be9398b9610a539360353fd61b521732d4))
|
||||
* **python:** inline script metadata (PEP 723) ([#5712](https://github.com/windmill-labs/windmill/issues/5712)) ([2622253](https://github.com/windmill-labs/windmill/commit/26222539e66bce7e88f86a7e5917e6ca99350865))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing http_trigger_version_seq grants ([#5816](https://github.com/windmill-labs/windmill/issues/5816)) ([306f3ea](https://github.com/windmill-labs/windmill/commit/306f3eabd1c03fa904b0e59438de124a0e680597))
|
||||
* avoid monaco memory leak ([0d459d5](https://github.com/windmill-labs/windmill/commit/0d459d5d223728270854e37715ecc1663ede9870))
|
||||
* error handler node rendering at top level ([feae9b0](https://github.com/windmill-labs/windmill/commit/feae9b09240ba306c007013a36d2aefb0b273766))
|
||||
* **frontend:** auto completion and render of tailwind classes in app editor ([#5817](https://github.com/windmill-labs/windmill/issues/5817)) ([5897e7e](https://github.com/windmill-labs/windmill/commit/5897e7e01b8839425c30c2a97481ef7bb9090661))
|
||||
|
||||
## [1.492.1](https://github.com/windmill-labs/windmill/compare/v1.492.0...v1.492.1) (2025-05-22)
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -5,4 +5,5 @@ oauth2.json
|
||||
tracing.folded
|
||||
heaptrack*
|
||||
index/
|
||||
windmill-api/openapi-*.*
|
||||
windmill-api/openapi-*.*
|
||||
.duckdb/*
|
||||
Generated
+403
-187
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.492.1"
|
||||
version = "1.493.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -32,7 +32,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.492.1"
|
||||
version = "1.493.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -87,13 +87,14 @@ python = ["windmill-worker/python", "windmill-api/python"]
|
||||
rust = ["windmill-worker/rust"]
|
||||
mysql = ["windmill-worker/mysql"]
|
||||
oracledb = ["windmill-worker/oracledb"]
|
||||
duckdb = ["windmill-worker/duckdb"]
|
||||
mssql = ["windmill-worker/mssql"]
|
||||
bigquery = ["windmill-worker/bigquery"]
|
||||
php = ["windmill-worker/php"]
|
||||
csharp = ["windmill-worker/csharp"]
|
||||
nu = ["windmill-worker/nu"]
|
||||
java = ["windmill-worker/java"]
|
||||
all_languages = [ "python", "deno_core", "rust", "mysql", "oracledb", "mssql", "bigquery", "csharp", "nu", "php", "java"]
|
||||
all_languages = [ "python", "deno_core", "rust", "mysql", "oracledb", "duckdb", "mssql", "bigquery", "csharp", "nu", "php", "java"]
|
||||
|
||||
|
||||
[patch.crates-io]
|
||||
@@ -238,6 +239,7 @@ json-pointer = "^0"
|
||||
itertools = "^0"
|
||||
regex = "^1"
|
||||
semver = "^1"
|
||||
duckdb = { version = "1.2.2", features = ["bundled"] }
|
||||
|
||||
v8 = "=130.0.7" # Exact version NOTE: Do not forget to update version and hash in flake.nix
|
||||
deno_fetch = "0.214.0"
|
||||
|
||||
@@ -1 +1 @@
|
||||
6899b8151329218a1df59964dac57e0e004ae25a
|
||||
32039f675060b5996951708368bdefe14278d5cd
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add up migration script here
|
||||
ALTER TYPE SCRIPT_LANG ADD VALUE IF NOT EXISTS 'duckdb';
|
||||
UPDATE config set config = jsonb_set(config, '{worker_tags}', config->'worker_tags' || '["duckdb"]'::jsonb) where name = 'worker__default' and config @> '{"worker_tags": ["deno", "python3", "go", "bash", "powershell", "dependency", "flow", "hub", "other", "bun", "php", "rust", "ansible", "csharp", "nu", "java"]}'::jsonb AND NOT config->'worker_tags' @> '"duckdb"'::jsonb;
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add up migration script here
|
||||
GRANT ALL ON SEQUENCE http_trigger_version_seq TO windmill_user;
|
||||
GRANT ALL ON SEQUENCE http_trigger_version_seq TO windmill_admin;
|
||||
@@ -83,6 +83,21 @@ pub fn parse_bigquery_sig(code: &str) -> anyhow::Result<MainArgSignature> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_duckdb_sig(code: &str) -> anyhow::Result<MainArgSignature> {
|
||||
let parsed = parse_duckdb_file(&code)?;
|
||||
if let Some(args) = parsed {
|
||||
Ok(MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args,
|
||||
no_main_func: None,
|
||||
has_preprocessor: None,
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("Error parsing sql".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_snowflake_sig(code: &str) -> anyhow::Result<MainArgSignature> {
|
||||
let parsed = parse_snowflake_file(&code)?;
|
||||
if let Some(x) = parsed {
|
||||
@@ -212,6 +227,9 @@ lazy_static::lazy_static! {
|
||||
// -- @name (type) = default
|
||||
static ref RE_ARG_BIGQUERY: Regex = Regex::new(r#"(?m)^-- @(\w+) \((\w+(?:\[\])?)\)(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
|
||||
|
||||
// -- $name (type) = default
|
||||
static ref RE_ARG_DUCKDB: 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();
|
||||
|
||||
|
||||
@@ -577,6 +595,35 @@ fn parse_bigquery_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
Ok(Some(args))
|
||||
}
|
||||
|
||||
fn parse_duckdb_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
let mut args: Vec<Arg> = vec![];
|
||||
|
||||
for cap in RE_ARG_DUCKDB.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_duckdb_typ(typ.as_str());
|
||||
|
||||
let parsed_default = default.and_then(|x| parsed_default(&parsed_typ, x));
|
||||
|
||||
args.push(Arg {
|
||||
name,
|
||||
typ: parsed_typ,
|
||||
default: parsed_default,
|
||||
otyp: Some(typ),
|
||||
has_default,
|
||||
oidx: None,
|
||||
});
|
||||
}
|
||||
|
||||
args.append(&mut parse_sql_sanitized_interpolation(code));
|
||||
Ok(Some(args))
|
||||
}
|
||||
|
||||
fn parse_snowflake_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
let mut args: Vec<Arg> = vec![];
|
||||
|
||||
@@ -729,6 +776,33 @@ pub fn parse_bigquery_typ(typ: &str) -> Typ {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_duckdb_typ(typ: &str) -> Typ {
|
||||
if typ.ends_with("[]") {
|
||||
let base_typ = parse_duckdb_typ(typ.strip_suffix("[]").unwrap());
|
||||
Typ::List(Box::new(base_typ))
|
||||
} else {
|
||||
match typ {
|
||||
"varchar" | "char" | "bpchar" | "text" | "string" => Typ::Str(None),
|
||||
"blob" | "bytea" | "binary" | "varbinary" | "bitstring" => Typ::Bytes,
|
||||
"boolean" | "bool" | "bit" | "logical" => Typ::Bool,
|
||||
"bigint" | "int8" | "long" | "integer" | "int4" | "int" | "smallint" | "int2"
|
||||
| "short" | "tinyint" | "int1" | "signed" | "ubigint" | "uhugeint" | "uinteger"
|
||||
| "usmallint" | "utinyint" => Typ::Int,
|
||||
"decimal" | "numeric" | "double" | "float8" | "float" | "float4" | "real" => Typ::Float,
|
||||
"date"
|
||||
| "time"
|
||||
| "timestamp with time zone"
|
||||
| "timestamptz"
|
||||
| "timestamp"
|
||||
| "datetime" => Typ::Datetime,
|
||||
"uuid" | "json" => Typ::Str(None),
|
||||
"interval" | "hugeint" => Typ::Str(None),
|
||||
"s3object" => Typ::Resource("S3Object".to_string()),
|
||||
_ => Typ::Str(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_snowflake_typ(typ: &str) -> Typ {
|
||||
match typ {
|
||||
"varchar" => Typ::Str(None),
|
||||
|
||||
@@ -96,6 +96,12 @@ pub fn parse_oracledb(code: &str) -> String {
|
||||
wrap_sig(windmill_parser_sql::parse_oracledb_sig(code))
|
||||
}
|
||||
|
||||
#[cfg(feature = "sql-parser")]
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_duckdb(code: &str) -> String {
|
||||
wrap_sig(windmill_parser_sql::parse_duckdb_sig(code))
|
||||
}
|
||||
|
||||
#[cfg(feature = "sql-parser")]
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_bigquery(code: &str) -> String {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.492.1
|
||||
version: 1.493.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -14242,7 +14242,8 @@ components:
|
||||
ansible,
|
||||
csharp,
|
||||
nu,
|
||||
java
|
||||
java,
|
||||
duckdb
|
||||
# for related places search: ADD_NEW_LANG
|
||||
]
|
||||
|
||||
|
||||
@@ -360,6 +360,7 @@ pub(crate) async fn tarball_workspace(
|
||||
ScriptLang::Bigquery => "bq.sql",
|
||||
ScriptLang::Snowflake => "sf.sql",
|
||||
ScriptLang::Mssql => "ms.sql",
|
||||
ScriptLang::DuckDb => "duckdb.sql",
|
||||
ScriptLang::Graphql => "gql",
|
||||
ScriptLang::Nativets => "fetch.ts",
|
||||
ScriptLang::Bun | ScriptLang::Bunnative => {
|
||||
|
||||
@@ -2,7 +2,11 @@ use anyhow::Context;
|
||||
use reqwest::{Body, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::utils::HTTP_CLIENT;
|
||||
use crate::{
|
||||
error::{self, to_anyhow},
|
||||
s3_helpers::{DuckdbConnectionSettingsQueryV2, DuckdbConnectionSettingsResponse},
|
||||
utils::HTTP_CLIENT,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuthedClient {
|
||||
@@ -197,4 +201,44 @@ impl AuthedClient {
|
||||
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default()))?,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_duckdb_connection_settings(
|
||||
&self,
|
||||
s3: &DuckdbConnectionSettingsQueryV2,
|
||||
) -> error::Result<DuckdbConnectionSettingsResponse> {
|
||||
let url = format!(
|
||||
"{}/api/w/{}/job_helpers/v2/duckdb_connection_settings",
|
||||
self.base_internal_url, &self.workspace
|
||||
);
|
||||
let response = self
|
||||
.force_client
|
||||
.as_ref()
|
||||
.unwrap_or(&HTTP_CLIENT)
|
||||
.post(url)
|
||||
.header(
|
||||
reqwest::header::CONTENT_TYPE,
|
||||
reqwest::header::HeaderValue::from_static("application/json"),
|
||||
)
|
||||
.header(
|
||||
reqwest::header::ACCEPT,
|
||||
reqwest::header::HeaderValue::from_static("application/json"),
|
||||
)
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
reqwest::header::HeaderValue::from_str(&format!("Bearer {}", self.token))
|
||||
.map_err(|e| error::Error::BadConfig(e.to_string()))?,
|
||||
)
|
||||
.body(serde_json::to_string(&s3).map_err(to_anyhow)?)
|
||||
.send()
|
||||
.await
|
||||
.context(format!("Sent get_duckdb_connection_settings request",))
|
||||
.map_err(error::Error::from)?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => Ok(response
|
||||
.json::<DuckdbConnectionSettingsResponse>()
|
||||
.await
|
||||
.context("decoding duckdb_connection_settings response as json")?),
|
||||
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default()))?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -891,3 +891,20 @@ pub async fn convert_json_line_stream<E: Into<anyhow::Error>>(
|
||||
|
||||
Ok(tokio_stream::wrappers::ReceiverStream::new(rx))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct DuckdbConnectionSettingsResponse {
|
||||
pub connection_settings_str: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub azure_container_path: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub s3_bucket: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct DuckdbConnectionSettingsQueryV2 {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub s3_resource_path: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub storage: Option<String>,
|
||||
}
|
||||
|
||||
@@ -349,7 +349,7 @@ pub fn should_validate_schema(code: &str, lang: &ScriptLang) -> bool {
|
||||
let comment = match lang {
|
||||
Nativets | Bun | Bunnative | Deno | Php | CSharp | Java => "//",
|
||||
Python3 | Go | Bash | Powershell | Graphql | Ansible | Nu => "#",
|
||||
Postgresql | Mysql | Bigquery | Snowflake | Mssql | OracleDB => "--",
|
||||
Postgresql | Mysql | Bigquery | Snowflake | Mssql | OracleDB | DuckDb => "--",
|
||||
Rust => "//!",
|
||||
// for related places search: ADD_NEW_LANG
|
||||
};
|
||||
|
||||
@@ -46,13 +46,13 @@ pub enum ScriptLang {
|
||||
Graphql,
|
||||
Mssql,
|
||||
OracleDB,
|
||||
DuckDb,
|
||||
Php,
|
||||
Rust,
|
||||
Ansible,
|
||||
CSharp,
|
||||
Nu,
|
||||
Java,
|
||||
// for related places search: ADD_NEW_LANG
|
||||
Java, // for related places search: ADD_NEW_LANG
|
||||
}
|
||||
|
||||
impl ScriptLang {
|
||||
@@ -73,6 +73,7 @@ impl ScriptLang {
|
||||
ScriptLang::Mssql => "mssql",
|
||||
ScriptLang::Graphql => "graphql",
|
||||
ScriptLang::OracleDB => "oracledb",
|
||||
ScriptLang::DuckDb => "duckdb",
|
||||
ScriptLang::Php => "php",
|
||||
ScriptLang::Rust => "rust",
|
||||
ScriptLang::Ansible => "ansible",
|
||||
|
||||
@@ -79,6 +79,7 @@ lazy_static::lazy_static! {
|
||||
"csharp".to_string(),
|
||||
"nu".to_string(),
|
||||
"java".to_string(),
|
||||
"duckdb".to_string(),
|
||||
// for related places search: ADD_NEW_LANG
|
||||
"dependency".to_string(),
|
||||
"flow".to_string(),
|
||||
|
||||
@@ -31,6 +31,7 @@ csharp = ["dep:windmill-parser-csharp"]
|
||||
rust = ["dep:windmill-parser-rust"]
|
||||
nu = ["dep:windmill-parser-nu"]
|
||||
java = ["dep:windmill-parser-java"]
|
||||
duckdb = []
|
||||
|
||||
[dependencies]
|
||||
windmill-queue.workspace = true
|
||||
@@ -92,6 +93,7 @@ deno_permissions = { workspace = true, optional = true }
|
||||
deno_io = { workspace = true, optional = true }
|
||||
deno_error = { workspace = true, optional = true }
|
||||
async-stream.workspace = true
|
||||
duckdb.workspace = true
|
||||
|
||||
postgres-native-tls.workspace = true
|
||||
native-tls.workspace = true
|
||||
|
||||
@@ -0,0 +1,664 @@
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
|
||||
use duckdb::types::TimeUnit;
|
||||
use duckdb::{params_from_iter, Row};
|
||||
use rust_decimal::prelude::FromPrimitive;
|
||||
use rust_decimal::Decimal;
|
||||
use serde_json::value::RawValue;
|
||||
use serde_json::{json, Value};
|
||||
use tokio::fs::remove_file;
|
||||
use tokio::task;
|
||||
use uuid::Uuid;
|
||||
use windmill_common::error::{to_anyhow, Error, Result};
|
||||
use windmill_common::s3_helpers::{
|
||||
DuckdbConnectionSettingsQueryV2, DuckdbConnectionSettingsResponse, S3Object,
|
||||
};
|
||||
use windmill_common::worker::{to_raw_value, Connection};
|
||||
use windmill_parser_sql::{parse_duckdb_sig, parse_sql_blocks};
|
||||
use windmill_queue::{CanceledBy, MiniPulledJob};
|
||||
|
||||
use crate::common::{build_args_values, OccupancyMetrics};
|
||||
use crate::handle_child::run_future_with_polling_update_job_poller;
|
||||
#[cfg(feature = "mysql")]
|
||||
use crate::mysql_executor::MysqlDatabase;
|
||||
use crate::pg_executor::PgDatabase;
|
||||
use crate::sanitized_sql_params::sanitize_and_interpolate_unsafe_sql_args;
|
||||
use windmill_common::client::AuthedClient;
|
||||
|
||||
fn do_duckdb_inner(
|
||||
conn: &duckdb::Connection,
|
||||
query: &str,
|
||||
job_args: &HashMap<String, duckdb::types::Value>,
|
||||
skip_collect: bool,
|
||||
column_order: &mut Option<Vec<String>>,
|
||||
) -> Result<Box<RawValue>> {
|
||||
let mut rows_vec = vec![];
|
||||
|
||||
let (query, job_args) = interpolate_named_args(query, &job_args);
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(&query)
|
||||
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
|
||||
|
||||
let mut rows = stmt
|
||||
.query(params_from_iter(job_args))
|
||||
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
|
||||
|
||||
if skip_collect {
|
||||
return Ok(to_raw_value(&json!([])));
|
||||
}
|
||||
|
||||
// Statement needs to be stepped at least once or stmt.column_names() will panic
|
||||
let mut column_names = None;
|
||||
loop {
|
||||
let row = rows.next();
|
||||
match row {
|
||||
Ok(Some(row)) => {
|
||||
// Set column names if not already set
|
||||
let stmt = row.as_ref();
|
||||
let column_names = match column_names.as_ref() {
|
||||
Some(column_names) => column_names,
|
||||
None => {
|
||||
column_names = Some(stmt.column_names());
|
||||
column_names.as_ref().unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
let row = row_to_value(row, &column_names.as_slice())
|
||||
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
|
||||
rows_vec.push(row);
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
return Err(Error::ExecutionErr(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(column_order), Some(column_names)) = (column_order.as_mut(), column_names) {
|
||||
*column_order = column_names.clone();
|
||||
}
|
||||
|
||||
return Ok(to_raw_value(&rows_vec));
|
||||
}
|
||||
|
||||
pub async fn do_duckdb(
|
||||
job: &MiniPulledJob,
|
||||
client: &AuthedClient,
|
||||
query: &str,
|
||||
conn: &Connection,
|
||||
mem_peak: &mut i32,
|
||||
canceled_by: &mut Option<CanceledBy>,
|
||||
worker_name: &str,
|
||||
column_order_ref: &mut Option<Vec<String>>,
|
||||
occupancy_metrics: &mut OccupancyMetrics,
|
||||
) -> Result<Box<RawValue>> {
|
||||
let result_f = async {
|
||||
let sig = parse_duckdb_sig(query)?.args;
|
||||
let mut job_args = build_args_values(job, client, conn).await?;
|
||||
|
||||
let (query, _) = &sanitize_and_interpolate_unsafe_sql_args(query, &sig, &job_args)?;
|
||||
// Prevent interpolate_named_args from detecting argument identifiers in the signature for
|
||||
// the first query block
|
||||
let query = trunc_sig(query);
|
||||
|
||||
let (_query_with_transformed_s3_uris, mut used_storages) =
|
||||
transform_s3_uris(query, client).await?;
|
||||
let query = _query_with_transformed_s3_uris.as_deref().unwrap_or(query);
|
||||
|
||||
let job_args = {
|
||||
let mut m: HashMap<String, duckdb::types::Value> = HashMap::new();
|
||||
for sig_arg in sig.into_iter() {
|
||||
let json_value = job_args
|
||||
.remove(&sig_arg.name)
|
||||
.or_else(|| sig_arg.default)
|
||||
.unwrap_or_else(|| json!(null));
|
||||
|
||||
if matches!(&sig_arg.otyp.as_ref().map(String::as_str), Some("s3object")) {
|
||||
let s3_obj = serde_json::from_value::<S3Object>(json_value).map_err(|e| {
|
||||
Error::ExecutionErr(format!("Failed to deserialize S3Object: {}", e))
|
||||
})?;
|
||||
let duckdb_conn_settings: windmill_common::s3_helpers::DuckdbConnectionSettingsResponse = client
|
||||
.get_duckdb_connection_settings(&DuckdbConnectionSettingsQueryV2 {
|
||||
s3_resource_path: None,
|
||||
storage: s3_obj.storage.clone(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let uri = match (
|
||||
&duckdb_conn_settings.s3_bucket,
|
||||
&duckdb_conn_settings.azure_container_path,
|
||||
) {
|
||||
(Some(s3_bucket), None) => format!("s3://{}/{}", s3_bucket, &s3_obj.s3),
|
||||
(None, Some(az_container)) => format!("{}/{}", az_container, &s3_obj.s3),
|
||||
_ => {
|
||||
return Err(Error::ExecutionErr(
|
||||
"S3Object must have either s3_bucket or azure_container_path"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
m.insert(sig_arg.name, duckdb::types::Value::Text(uri));
|
||||
used_storages.insert(s3_obj.storage, duckdb_conn_settings);
|
||||
} else {
|
||||
let duckdb_value = json_value_to_duckdb_value(
|
||||
&json_value,
|
||||
sig_arg
|
||||
.otyp
|
||||
.clone()
|
||||
.unwrap_or_else(|| "text".to_string())
|
||||
.as_str(),
|
||||
client,
|
||||
)?;
|
||||
m.insert(sig_arg.name, duckdb_value);
|
||||
}
|
||||
}
|
||||
m
|
||||
};
|
||||
|
||||
let query_block_list = parse_sql_blocks(query);
|
||||
|
||||
// Replace windmill resource ATTACH statements with the real instructions
|
||||
let query_block_list = {
|
||||
let mut v = vec![];
|
||||
for query_block in query_block_list.iter() {
|
||||
match parse_attach_db_resource(query_block) {
|
||||
Some(parsed) => v.extend(
|
||||
transform_attach_db_resource_query(&parsed, &job.id, client).await?,
|
||||
),
|
||||
None => v.push(query_block.to_string()),
|
||||
};
|
||||
}
|
||||
v
|
||||
};
|
||||
|
||||
// duckdb::Connection is not Send so we do it in a single blocking task
|
||||
let (result, column_order) = task::spawn_blocking(move || {
|
||||
let conn = duckdb::Connection::open_in_memory()
|
||||
.map_err(|e| Error::ConnectingToDatabase(e.to_string()))?;
|
||||
|
||||
for (_, DuckdbConnectionSettingsResponse { connection_settings_str, .. }) in
|
||||
used_storages.into_iter()
|
||||
{
|
||||
conn.execute_batch(&connection_settings_str)
|
||||
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
|
||||
}
|
||||
|
||||
let mut result: Option<Box<RawValue>> = None;
|
||||
let mut column_order = None;
|
||||
for (query_block_index, query_block) in query_block_list.iter().enumerate() {
|
||||
result = Some(
|
||||
do_duckdb_inner(
|
||||
&conn,
|
||||
query_block.as_str(),
|
||||
&job_args,
|
||||
query_block_index != query_block_list.len() - 1,
|
||||
&mut column_order,
|
||||
)
|
||||
.map_err(|e| Error::ExecutionErr(e.to_string()))?,
|
||||
);
|
||||
}
|
||||
let result = result.unwrap_or_else(|| to_raw_value(&json!([])));
|
||||
Ok::<_, Error>((result, column_order))
|
||||
})
|
||||
.await
|
||||
.map_err(to_anyhow)??;
|
||||
|
||||
*column_order_ref = column_order;
|
||||
|
||||
// BigQuery cleanup
|
||||
let bq_credentials_path = make_bq_credentials_path(&job.id);
|
||||
env::remove_var("GOOGLE_APPLICATION_CREDENTIALS");
|
||||
if matches!(tokio::fs::try_exists(&bq_credentials_path).await, Ok(true)) {
|
||||
remove_file(&bq_credentials_path).await.map_err(to_anyhow)?;
|
||||
}
|
||||
Ok(result)
|
||||
};
|
||||
|
||||
let result = run_future_with_polling_update_job_poller(
|
||||
job.id,
|
||||
job.timeout,
|
||||
conn,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
result_f,
|
||||
worker_name,
|
||||
&job.workspace_id,
|
||||
&mut Some(occupancy_metrics),
|
||||
Box::pin(futures::stream::once(async { 0 })),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn row_to_value(row: &Row<'_>, column_names: &[String]) -> Result<Box<RawValue>> {
|
||||
let mut obj = serde_json::Map::new();
|
||||
for (i, key) in column_names.iter().enumerate() {
|
||||
let value: duckdb::types::Value =
|
||||
row.get(i).map_err(|e| Error::ExecutionErr(e.to_string()))?;
|
||||
let json_value = match value {
|
||||
duckdb::types::Value::Null => serde_json::Value::Null,
|
||||
duckdb::types::Value::Boolean(b) => serde_json::Value::Bool(b),
|
||||
duckdb::types::Value::TinyInt(i) => serde_json::Value::Number(i.into()),
|
||||
duckdb::types::Value::SmallInt(i) => serde_json::Value::Number(i.into()),
|
||||
duckdb::types::Value::Int(i) => serde_json::Value::Number(i.into()),
|
||||
duckdb::types::Value::BigInt(i) => serde_json::Value::Number(i.into()),
|
||||
duckdb::types::Value::HugeInt(i) => serde_json::Value::String(i.to_string()),
|
||||
duckdb::types::Value::UTinyInt(u) => serde_json::Value::Number(u.into()),
|
||||
duckdb::types::Value::USmallInt(u) => serde_json::Value::Number(u.into()),
|
||||
duckdb::types::Value::UInt(u) => serde_json::Value::Number(u.into()),
|
||||
duckdb::types::Value::UBigInt(u) => serde_json::Value::Number(u.into()),
|
||||
duckdb::types::Value::Float(f) => serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(f as f64)
|
||||
.ok_or_else(|| Error::ExecutionErr("Could not convert to f64".to_string()))?,
|
||||
),
|
||||
duckdb::types::Value::Double(f) => serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(f)
|
||||
.ok_or_else(|| Error::ExecutionErr("Could not convert to f64".to_string()))?,
|
||||
),
|
||||
duckdb::types::Value::Decimal(d) => serde_json::Value::String(d.to_string()),
|
||||
duckdb::types::Value::Timestamp(_, ts) => serde_json::Value::String(ts.to_string()),
|
||||
duckdb::types::Value::Text(s) => serde_json::Value::String(s),
|
||||
duckdb::types::Value::Blob(b) => serde_json::Value::Array(
|
||||
b.into_iter()
|
||||
.map(|byte| serde_json::Value::Number(byte.into()))
|
||||
.collect(),
|
||||
),
|
||||
duckdb::types::Value::Date32(d) => serde_json::Value::Number(d.into()),
|
||||
duckdb::types::Value::Time64(_, t) => serde_json::Value::String(t.to_string()),
|
||||
duckdb::types::Value::Interval { months, days, nanos } => serde_json::json!({
|
||||
"months": months,
|
||||
"days": days,
|
||||
"nanos": nanos
|
||||
}),
|
||||
duckdb::types::Value::List(values) => serde_json::Value::Array(
|
||||
values
|
||||
.into_iter()
|
||||
.map(|v| serde_json::Value::String(format!("{:?}", v)))
|
||||
.collect(),
|
||||
),
|
||||
duckdb::types::Value::Enum(e) => serde_json::Value::String(e),
|
||||
duckdb::types::Value::Struct(fields) => serde_json::Value::Object(
|
||||
fields
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), serde_json::Value::String(format!("{:?}", v))))
|
||||
.collect(),
|
||||
),
|
||||
duckdb::types::Value::Array(values) => serde_json::Value::Array(
|
||||
values
|
||||
.into_iter()
|
||||
.map(|v| serde_json::Value::String(format!("{:?}", v)))
|
||||
.collect(),
|
||||
),
|
||||
duckdb::types::Value::Map(map) => serde_json::Value::Object(
|
||||
map.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
format!("{:?}", k),
|
||||
serde_json::Value::String(format!("{:?}", v)),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
duckdb::types::Value::Union(value) => {
|
||||
serde_json::Value::String(format!("{:?}", *value))
|
||||
}
|
||||
};
|
||||
obj.insert(key.clone(), json_value);
|
||||
}
|
||||
serde_json::value::to_raw_value(&obj).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn json_value_to_duckdb_value(
|
||||
json_value: &serde_json::Value,
|
||||
arg_type: &str,
|
||||
client: &AuthedClient,
|
||||
) -> Result<duckdb::types::Value> {
|
||||
let arg_type = arg_type.to_lowercase();
|
||||
let duckdb_value = match json_value {
|
||||
serde_json::Value::Null => duckdb::types::Value::Null,
|
||||
serde_json::Value::Bool(b) => duckdb::types::Value::Boolean(*b),
|
||||
|
||||
serde_json::Value::String(s)
|
||||
if matches!(
|
||||
arg_type.as_str(),
|
||||
"timestamp" | "timestamptz" | "timestamp with time zone" | "datetime"
|
||||
) =>
|
||||
{
|
||||
string_to_duckdb_timestamp(&s)?
|
||||
}
|
||||
serde_json::Value::String(s) if arg_type.as_str() == "date" => string_to_duckdb_date(&s)?,
|
||||
serde_json::Value::String(s) if arg_type.as_str() == "time" => string_to_duckdb_time(&s)?,
|
||||
serde_json::Value::String(s) => duckdb::types::Value::Text(s.clone()),
|
||||
|
||||
serde_json::Value::Number(n) if n.is_i64() => {
|
||||
let v = n.as_i64().unwrap();
|
||||
match arg_type.as_str() {
|
||||
"tinyint" | "int1" => duckdb::types::Value::TinyInt(v as i8),
|
||||
"smallint" | "int2" | "short" => duckdb::types::Value::SmallInt(v as i16),
|
||||
"integer" | "int4" | "int" | "signed" => duckdb::types::Value::Int(v as i32),
|
||||
"bigint" | "int8" | "long" => duckdb::types::Value::BigInt(v),
|
||||
"hugeint" => duckdb::types::Value::HugeInt(v as i128),
|
||||
"float" | "float4" | "real" => duckdb::types::Value::Float(v as f32),
|
||||
"double" | "float8" => duckdb::types::Value::Double(v as f64),
|
||||
_ => duckdb::types::Value::BigInt(v), // default fallback
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::Value::Number(n) if n.is_u64() => {
|
||||
let v = n.as_u64().unwrap();
|
||||
match arg_type.as_str() {
|
||||
"utinyint" => duckdb::types::Value::UTinyInt(v as u8),
|
||||
"usmallint" => duckdb::types::Value::USmallInt(v as u16),
|
||||
"uinteger" => duckdb::types::Value::UInt(v as u32),
|
||||
"ubigint" | "uhugeint" => duckdb::types::Value::UBigInt(v),
|
||||
_ => duckdb::types::Value::UBigInt(v), // default fallback
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::Value::Number(n) if n.is_f64() => {
|
||||
let v = n.as_f64().unwrap();
|
||||
match arg_type.as_str() {
|
||||
"float" | "float4" | "real" => duckdb::types::Value::Float(v as f32),
|
||||
"double" | "float8" => duckdb::types::Value::Double(v),
|
||||
"decimal" | "numeric" => {
|
||||
duckdb::types::Value::Decimal(Decimal::from_f64(v).ok_or_else(|| {
|
||||
Error::ExecutionErr("Could not convert f64 to Decimal".to_string())
|
||||
})?)
|
||||
}
|
||||
_ => duckdb::types::Value::Double(v), // default fallback
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::Value::Array(arr) => duckdb::types::Value::Array(
|
||||
arr.iter()
|
||||
.map(|val| json_value_to_duckdb_value(val, arg_type.as_str(), client))
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
),
|
||||
serde_json::Value::Object(map) => duckdb::types::Value::Struct(
|
||||
map.iter()
|
||||
.map(|(k, v)| {
|
||||
Ok::<_, Error>((
|
||||
k.clone(),
|
||||
json_value_to_duckdb_value(v, arg_type.as_str(), client)?,
|
||||
))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.into(),
|
||||
),
|
||||
|
||||
value @ _ => {
|
||||
return Err(Error::ExecutionErr(format!(
|
||||
"Unsupported type in query: {:?} and signature {arg_type:?}",
|
||||
value
|
||||
)))
|
||||
}
|
||||
};
|
||||
Ok(duckdb_value)
|
||||
}
|
||||
|
||||
fn string_to_duckdb_timestamp(s: &str) -> Result<duckdb::types::Value> {
|
||||
let ts = chrono::DateTime::parse_from_rfc3339(s)
|
||||
.map_err(|e: chrono::ParseError| Error::ExecutionErr(e.to_string()))?;
|
||||
Ok(duckdb::types::Value::Timestamp(
|
||||
TimeUnit::Millisecond,
|
||||
ts.timestamp_millis(),
|
||||
))
|
||||
}
|
||||
|
||||
fn string_to_duckdb_date(s: &str) -> Result<duckdb::types::Value> {
|
||||
use chrono::Datelike;
|
||||
let date = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap();
|
||||
Ok(duckdb::types::Value::Date32(date.num_days_from_ce()))
|
||||
}
|
||||
|
||||
fn string_to_duckdb_time(s: &str) -> Result<duckdb::types::Value> {
|
||||
use chrono::Timelike;
|
||||
let time = chrono::NaiveTime::parse_from_str(s, "%H:%M:%S").unwrap();
|
||||
Ok(duckdb::types::Value::Time64(
|
||||
TimeUnit::Microsecond,
|
||||
time.num_seconds_from_midnight() as i64,
|
||||
))
|
||||
}
|
||||
|
||||
struct ParsedAttachDbResource<'a> {
|
||||
resource_path: &'a str,
|
||||
name: &'a str,
|
||||
db_type: &'a str,
|
||||
extra_args: Option<&'a str>,
|
||||
}
|
||||
fn parse_attach_db_resource<'a>(query: &'a str) -> Option<ParsedAttachDbResource<'a>> {
|
||||
lazy_static::lazy_static! {
|
||||
static ref RE: regex::Regex = regex::Regex::new(r"ATTACH '\$res:([^']+)' AS (\S+) \(TYPE (\w+)(.*)\)").unwrap();
|
||||
}
|
||||
|
||||
for cap in RE.captures_iter(query) {
|
||||
if let (Some(resource_path), Some(name), Some(db_type)) =
|
||||
(cap.get(1), cap.get(2), cap.get(3))
|
||||
{
|
||||
let extra_args = cap.get(4).map(|m| query[m.start()..m.end()].trim());
|
||||
return Some(ParsedAttachDbResource {
|
||||
resource_path: query[resource_path.start()..resource_path.end()].trim(),
|
||||
name: query[name.start()..name.end()].trim(),
|
||||
db_type: query[db_type.start()..db_type.end()].trim(),
|
||||
extra_args,
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn transform_attach_db_resource_query(
|
||||
parsed: &ParsedAttachDbResource<'_>,
|
||||
job_id: &Uuid,
|
||||
client: &AuthedClient,
|
||||
) -> Result<Vec<String>> {
|
||||
match parsed.db_type.to_lowercase().as_str() {
|
||||
"postgres" => {
|
||||
let resource: PgDatabase = client
|
||||
.get_resource_value_interpolated(parsed.resource_path, Some(job_id.to_string()))
|
||||
.await?;
|
||||
|
||||
let attach_str = format!(
|
||||
"ATTACH 'dbname={} {} host={} {} {}' AS {} (TYPE postgres{});",
|
||||
resource.dbname,
|
||||
resource
|
||||
.user
|
||||
.map(|u| format!("user={}", u))
|
||||
.unwrap_or_default(),
|
||||
resource.host,
|
||||
resource
|
||||
.password
|
||||
.map(|p| format!("password={}", p))
|
||||
.unwrap_or_default(),
|
||||
resource
|
||||
.port
|
||||
.map(|p| format!("port={}", p))
|
||||
.unwrap_or_default(),
|
||||
parsed.name,
|
||||
parsed.extra_args.unwrap_or("")
|
||||
);
|
||||
|
||||
Ok(vec![
|
||||
"INSTALL postgres;".to_string(),
|
||||
"LOAD postgres;".to_string(),
|
||||
attach_str,
|
||||
])
|
||||
}
|
||||
"mysql" => {
|
||||
#[cfg(not(feature = "mysql"))]
|
||||
return Err(Error::ExecutionErr(
|
||||
"MySQL feature is not enabled".to_string(),
|
||||
));
|
||||
|
||||
#[cfg(feature = "mysql")]
|
||||
{
|
||||
let resource: MysqlDatabase = client
|
||||
.get_resource_value_interpolated(parsed.resource_path, Some(job_id.to_string()))
|
||||
.await?;
|
||||
|
||||
let attach_str = format!(
|
||||
"ATTACH 'database={} host={} ssl_mode={} {} {} {}' AS {} (TYPE mysql{});",
|
||||
resource.database,
|
||||
resource.host,
|
||||
resource
|
||||
.ssl
|
||||
.map(|ssl| if ssl { "required" } else { "disabled" })
|
||||
.unwrap_or("preferred"),
|
||||
resource
|
||||
.password
|
||||
.map(|p| format!("password={}", p))
|
||||
.unwrap_or_default(),
|
||||
resource
|
||||
.port
|
||||
.map(|p| format!("port={}", p))
|
||||
.unwrap_or_default(),
|
||||
resource
|
||||
.user
|
||||
.map(|u| format!("user={}", u))
|
||||
.unwrap_or_default(),
|
||||
parsed.name,
|
||||
parsed.extra_args.unwrap_or("")
|
||||
);
|
||||
|
||||
Ok(vec![
|
||||
"INSTALL mysql;".to_string(),
|
||||
"LOAD mysql;".to_string(),
|
||||
attach_str,
|
||||
])
|
||||
}
|
||||
}
|
||||
"bigquery" => {
|
||||
let resource: Value = client
|
||||
.get_resource_value_interpolated(parsed.resource_path, Some(job_id.to_string()))
|
||||
.await?;
|
||||
// duckdb's bigquery extension requires a json file as credentials
|
||||
let bq_credentials_path = make_bq_credentials_path(job_id);
|
||||
env::set_var("GOOGLE_APPLICATION_CREDENTIALS", &bq_credentials_path);
|
||||
tokio::fs::write(&bq_credentials_path, resource.to_string())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::ExecutionErr(format!(
|
||||
"Failed to write BigQuery credentials to {}: {}",
|
||||
&bq_credentials_path, e
|
||||
))
|
||||
})?;
|
||||
let project_id: String = serde_json::from_value(
|
||||
resource
|
||||
.get("project_id")
|
||||
.ok_or_else(|| {
|
||||
Error::ExecutionErr("BigQuery resource must contain project_id".to_string())
|
||||
})?
|
||||
.to_owned(),
|
||||
)
|
||||
.map_err(|_e| Error::ExecutionErr("failed project_id deserialize".to_string()))?;
|
||||
let attach_str = format!(
|
||||
"ATTACH 'project={}' as {} (TYPE bigquery{});",
|
||||
project_id,
|
||||
parsed.name,
|
||||
parsed.extra_args.unwrap_or("")
|
||||
)
|
||||
.to_string();
|
||||
Ok(vec![
|
||||
"INSTALL bigquery FROM community;".to_string(),
|
||||
"LOAD bigquery;".to_string(),
|
||||
attach_str,
|
||||
])
|
||||
}
|
||||
_ => Err(Error::ExecutionErr(format!(
|
||||
"Unsupported db type in DuckDB ATTACH: {}",
|
||||
parsed.db_type
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the transformed query and the set of storages used
|
||||
async fn transform_s3_uris(
|
||||
query: &str,
|
||||
client: &AuthedClient,
|
||||
) -> Result<(
|
||||
Option<String>,
|
||||
HashMap<Option<String>, DuckdbConnectionSettingsResponse>,
|
||||
)> {
|
||||
let mut transformed_query = None;
|
||||
lazy_static::lazy_static! {
|
||||
static ref RE: regex::Regex = regex::Regex::new(r"'s3://([^'/]*)/([^']+)'").unwrap();
|
||||
}
|
||||
let mut used_storages = HashMap::new();
|
||||
for cap in RE.captures_iter(query) {
|
||||
if let (storage, Some(s3_path)) = (cap.get(1), cap.get(2)) {
|
||||
let s3_path = s3_path.as_str();
|
||||
let storage = match storage.map(|m| m.as_str()) {
|
||||
Some("") | None => None,
|
||||
Some(s) => Some(s.to_string()),
|
||||
};
|
||||
let original_str_lit =
|
||||
format!("'s3://{}/{}'", storage.as_deref().unwrap_or(""), s3_path);
|
||||
let duckdb_conn_settings = client
|
||||
.get_duckdb_connection_settings(&DuckdbConnectionSettingsQueryV2 {
|
||||
s3_resource_path: None,
|
||||
storage: storage.clone(),
|
||||
})
|
||||
.await?;
|
||||
let url = match &duckdb_conn_settings {
|
||||
DuckdbConnectionSettingsResponse { s3_bucket: Some(bucket), .. } => {
|
||||
format!("'s3://{bucket}/{s3_path}'")
|
||||
}
|
||||
DuckdbConnectionSettingsResponse { azure_container_path: Some(base), .. } => {
|
||||
format!("'{base}/{s3_path}'")
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::ExecutionErr(
|
||||
"DuckDB connection settings response must have either s3_bucket or azure_container_path".to_string(),
|
||||
))?;
|
||||
}
|
||||
};
|
||||
transformed_query = Some(
|
||||
transformed_query
|
||||
.unwrap_or(query.to_string())
|
||||
.replace(&original_str_lit, &url),
|
||||
);
|
||||
used_storages.insert(storage, duckdb_conn_settings);
|
||||
}
|
||||
}
|
||||
Ok((transformed_query, used_storages))
|
||||
}
|
||||
|
||||
// BigQuery extension requires a json file as credentials
|
||||
// The file path is set as an env var by do_duckdb
|
||||
// It is created by transform_attach_db_resource_query (when bigquery is detected)
|
||||
// and deleted by do_duckdb after the query is executed
|
||||
fn make_bq_credentials_path(job_id: &Uuid) -> String {
|
||||
format!("/tmp/service-account-credentials-{}.json", job_id)
|
||||
}
|
||||
|
||||
// duckdb-rs does not support named parameters,
|
||||
// and it raises an error when passing unused arguments. We cannot prepare batch statements
|
||||
// but only single SQL statements so it doesn't work when all arguments are not used by
|
||||
// every single statement.
|
||||
fn interpolate_named_args<'a>(
|
||||
query: &str,
|
||||
args: &'a HashMap<String, duckdb::types::Value>,
|
||||
) -> (String, Vec<&'a duckdb::types::Value>) {
|
||||
let mut query = query.to_string();
|
||||
|
||||
let mut values = vec![];
|
||||
for (arg_name, arg_value) in args {
|
||||
let pat = format!("${}", arg_name);
|
||||
if !query.contains(&pat) {
|
||||
continue;
|
||||
}
|
||||
values.push(arg_value);
|
||||
query = query.replace(&pat, &format!("${}", values.len()));
|
||||
}
|
||||
(query, values)
|
||||
}
|
||||
|
||||
fn trunc_sig(query: &str) -> &str {
|
||||
let idx = query.rfind("-- $").unwrap_or(query.len());
|
||||
// find next \n starting from idx and return everything after it
|
||||
let idx = query[idx..].find('\n').map(|i| i + idx).unwrap_or(0);
|
||||
&query[idx..]
|
||||
}
|
||||
@@ -20,6 +20,8 @@ mod csharp_executor;
|
||||
#[cfg(feature = "enterprise")]
|
||||
mod dedicated_worker;
|
||||
mod deno_executor;
|
||||
#[cfg(feature = "duckdb")]
|
||||
mod duckdb_executor;
|
||||
mod global_cache;
|
||||
mod go_executor;
|
||||
mod graphql_executor;
|
||||
|
||||
@@ -32,13 +32,13 @@ use crate::{
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MysqlDatabase {
|
||||
host: String,
|
||||
user: Option<String>,
|
||||
password: Option<String>,
|
||||
port: Option<u16>,
|
||||
database: String,
|
||||
ssl: Option<bool>,
|
||||
pub struct MysqlDatabase {
|
||||
pub host: String,
|
||||
pub user: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub port: Option<u16>,
|
||||
pub database: String,
|
||||
pub ssl: Option<bool>,
|
||||
}
|
||||
|
||||
fn do_mysql_inner<'a>(
|
||||
|
||||
@@ -47,14 +47,14 @@ use lazy_static::lazy_static;
|
||||
use urlencoding::encode;
|
||||
use windmill_common::client::AuthedClient;
|
||||
#[derive(Deserialize)]
|
||||
struct PgDatabase {
|
||||
host: String,
|
||||
user: Option<String>,
|
||||
password: Option<String>,
|
||||
port: Option<u16>,
|
||||
sslmode: Option<String>,
|
||||
dbname: String,
|
||||
root_certificate_pem: Option<String>,
|
||||
pub struct PgDatabase {
|
||||
pub host: String,
|
||||
pub user: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub port: Option<u16>,
|
||||
pub sslmode: Option<String>,
|
||||
pub dbname: String,
|
||||
pub root_certificate_pem: Option<String>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
|
||||
@@ -2998,6 +2998,8 @@ var $RawScript = {
|
||||
"mssql",
|
||||
"graphql",
|
||||
"nativets",
|
||||
"duckdb",
|
||||
// for related places search: ADD_NEW_LANG
|
||||
],
|
||||
},
|
||||
path: {
|
||||
|
||||
@@ -145,6 +145,9 @@ use crate::ansible_executor::handle_ansible_job;
|
||||
#[cfg(feature = "mysql")]
|
||||
use crate::mysql_executor::do_mysql;
|
||||
|
||||
#[cfg(feature = "duckdb")]
|
||||
use crate::duckdb_executor::do_duckdb;
|
||||
|
||||
#[cfg(feature = "oracledb")]
|
||||
use crate::oracledb_executor::do_oracledb;
|
||||
|
||||
@@ -2614,6 +2617,30 @@ async fn handle_code_execution_job(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
} else if language == Some(ScriptLang::DuckDb) {
|
||||
#[allow(unreachable_code)]
|
||||
#[cfg(not(feature = "duckdb"))]
|
||||
{
|
||||
return Err(Error::internal_err(
|
||||
"Duck DB requires the duckdb feature to be enabled".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "duckdb")]
|
||||
{
|
||||
return do_duckdb(
|
||||
job,
|
||||
&client,
|
||||
&code,
|
||||
conn,
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
worker_name,
|
||||
column_order,
|
||||
occupancy_metrics,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
} else if language == Some(ScriptLang::Graphql) {
|
||||
return do_graphql(
|
||||
job,
|
||||
@@ -3021,6 +3048,7 @@ fn parse_sig_of_lang(
|
||||
ScriptLang::Snowflake => Some(windmill_parser_sql::parse_snowflake_sig(code)?),
|
||||
ScriptLang::Graphql => None,
|
||||
ScriptLang::Mssql => Some(windmill_parser_sql::parse_mssql_sig(code)?),
|
||||
ScriptLang::DuckDb => Some(windmill_parser_sql::parse_duckdb_sig(code)?),
|
||||
ScriptLang::OracleDB => Some(windmill_parser_sql::parse_oracledb_sig(code)?),
|
||||
#[cfg(feature = "php")]
|
||||
ScriptLang::Php => Some(windmill_parser_php::parse_php_signature(
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.492.1";
|
||||
export const VERSION = "v1.493.0";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -76,6 +76,7 @@ func main() (interface{}, error) {
|
||||
|
||||
bash: `echo "Hello world"
|
||||
`,
|
||||
duckdb: `SELECT 'Hello world' AS message`,
|
||||
|
||||
oracledb: `SELECT 'Hello world' AS message`,
|
||||
powershell: `Write-Output "Hello world"`,
|
||||
@@ -126,5 +127,5 @@ public class Main {
|
||||
}
|
||||
}
|
||||
`,
|
||||
// for related places search: ADD_NEW_LANG
|
||||
// for related places search: ADD_NEW_LANG
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
# Note for mac OS users: you need to install gnu-sed with `brew install gnu-sed` and use `gsed` instead of `sed`.
|
||||
./gen_wm_client.sh
|
||||
deno run -A dnt.ts
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.492.1";
|
||||
export const VERSION = "1.493.0";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
@@ -528,6 +528,9 @@ export async function inferSchema(
|
||||
{ name: "database", typ: { resource: "postgresql" } },
|
||||
...inferedSchema.args,
|
||||
];
|
||||
} else if (language === "duckdb") {
|
||||
const { parse_sql } = await import("./wasm/regex/windmill_parser_wasm.js");
|
||||
inferedSchema = JSON.parse(parse_sql(content));
|
||||
} else if (language === "graphql") {
|
||||
const { parse_graphql } = await import(
|
||||
"./wasm/regex/windmill_parser_wasm.js"
|
||||
|
||||
+6
-4
@@ -547,6 +547,8 @@ export function filePathExtensionFromContentType(
|
||||
return ".my.sql";
|
||||
} else if (language === "bigquery") {
|
||||
return ".bq.sql";
|
||||
} else if (language === "duckdb") {
|
||||
return ".duckdb.sql";
|
||||
} else if (language === "oracledb") {
|
||||
return ".odb.sql";
|
||||
} else if (language === "snowflake") {
|
||||
@@ -573,7 +575,7 @@ export function filePathExtensionFromContentType(
|
||||
return ".nu";
|
||||
} else if (language === "java") {
|
||||
return ".java";
|
||||
// for related places search: ADD_NEW_LANG
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else {
|
||||
throw new Error("Invalid language: " + language);
|
||||
}
|
||||
@@ -593,6 +595,7 @@ export const exts = [
|
||||
".odb.sql",
|
||||
".sf.sql",
|
||||
".ms.sql",
|
||||
".duckdb.sql",
|
||||
".sql",
|
||||
".gql",
|
||||
".ps1",
|
||||
@@ -601,9 +604,8 @@ export const exts = [
|
||||
".cs",
|
||||
".nu",
|
||||
".playbook.yml",
|
||||
".java"
|
||||
// for related places search: ADD_NEW_LANG
|
||||
|
||||
".java",
|
||||
// for related places search: ADD_NEW_LANG
|
||||
];
|
||||
|
||||
export function removeExtensionToPath(path: string): string {
|
||||
|
||||
@@ -9,6 +9,7 @@ export type ScriptLanguage =
|
||||
| "postgresql"
|
||||
| "mysql"
|
||||
| "bigquery"
|
||||
| "duckdb"
|
||||
| "oracledb"
|
||||
| "snowflake"
|
||||
| "mssql"
|
||||
@@ -19,7 +20,7 @@ export type ScriptLanguage =
|
||||
| "nu"
|
||||
| "ansible"
|
||||
| "java";
|
||||
// for related places search: ADD_NEW_LANG
|
||||
// for related places search: ADD_NEW_LANG
|
||||
|
||||
export function inferContentTypeFromFilePath(
|
||||
contentPath: string,
|
||||
@@ -43,6 +44,8 @@ export function inferContentTypeFromFilePath(
|
||||
return "bigquery";
|
||||
} else if (contentPath.endsWith(".odb.sql")) {
|
||||
return "oracledb";
|
||||
} else if (contentPath.endsWith(".duckdb.sql")) {
|
||||
return "duckdb";
|
||||
} else if (contentPath.endsWith(".sf.sql")) {
|
||||
return "snowflake";
|
||||
} else if (contentPath.endsWith(".ms.sql")) {
|
||||
@@ -67,7 +70,7 @@ export function inferContentTypeFromFilePath(
|
||||
return "nu";
|
||||
} else if (contentPath.endsWith(".java")) {
|
||||
return "java";
|
||||
// for related places search: ADD_NEW_LANG
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid language: " + contentPath.substring(contentPath.lastIndexOf("."))
|
||||
|
||||
@@ -344,6 +344,7 @@ export function newPathAssigner(defaultTs: "bun" | "deno"): PathAssigner {
|
||||
else if (language == "nu") ext = "nu";
|
||||
else if (language == "ansible") ext = "playbook.yml";
|
||||
else if (language == "java") ext = "java";
|
||||
else if (language == "duckdb") ext = "duckdb.sql";
|
||||
// for related places search: ADD_NEW_LANG
|
||||
else ext = "no_ext";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
|
||||
Generated
+6
-6
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.492.1",
|
||||
"version": "1.493.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.492.1",
|
||||
"version": "1.493.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
@@ -74,7 +74,7 @@
|
||||
"windmill-parser-wasm-nu": "^1.474.1",
|
||||
"windmill-parser-wasm-php": "^1.429.0",
|
||||
"windmill-parser-wasm-py": "^1.477.1",
|
||||
"windmill-parser-wasm-regex": "^1.481.0",
|
||||
"windmill-parser-wasm-regex": "^1.492.1",
|
||||
"windmill-parser-wasm-rust": "^1.429.0",
|
||||
"windmill-parser-wasm-ts": "^1.486.1",
|
||||
"windmill-parser-wasm-yaml": "^1.429.0",
|
||||
@@ -12837,9 +12837,9 @@
|
||||
"integrity": "sha512-EY3mSMWpqFPzd7fsLg2/hSfQFU8HpW9nplFwm4JHHCDbcTpBzlvzjPJoHAAGO5kMzowAxjqi5ai/mXjeUWuiSg=="
|
||||
},
|
||||
"node_modules/windmill-parser-wasm-regex": {
|
||||
"version": "1.481.0",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.481.0.tgz",
|
||||
"integrity": "sha512-QY9NOaF0M55SBEbbcAkHoXppjdS2gb2NlwqhlZ1ciYWTtIrbZoL5K8xoG4FbB5vWRHxInG1aRdA2tb+JP180yA=="
|
||||
"version": "1.492.1",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.492.1.tgz",
|
||||
"integrity": "sha512-CBdjz3x00z2xA4BRZv8/hsuHcpZgL28UKTjYVSj5O9XMYlY9g0LNsEQ5TTl/oVyFBs62/CPyvWwGvW4oSAObzg=="
|
||||
},
|
||||
"node_modules/windmill-parser-wasm-rust": {
|
||||
"version": "1.429.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.492.1",
|
||||
"version": "1.493.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
@@ -143,7 +143,7 @@
|
||||
"windmill-parser-wasm-nu": "^1.474.1",
|
||||
"windmill-parser-wasm-php": "^1.429.0",
|
||||
"windmill-parser-wasm-py": "^1.477.1",
|
||||
"windmill-parser-wasm-regex": "^1.481.0",
|
||||
"windmill-parser-wasm-regex": "^1.492.1",
|
||||
"windmill-parser-wasm-rust": "^1.429.0",
|
||||
"windmill-parser-wasm-ts": "^1.486.1",
|
||||
"windmill-parser-wasm-yaml": "^1.429.0",
|
||||
@@ -371,6 +371,10 @@
|
||||
"types": "./package/components/SimpleEditor.svelte.d.ts",
|
||||
"svelte": "./package/components/SimpleEditor.svelte",
|
||||
"default": "./package/components/SimpleEditor.svelte"
|
||||
},
|
||||
"./tailwindUtils": {
|
||||
"types": "./package/components/apps/editor/componentsPanel/tailwindUtils.d.ts",
|
||||
"default": "./package/components/apps/editor/componentsPanel/tailwindUtils.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -506,6 +510,9 @@
|
||||
],
|
||||
"components/icons/store": [
|
||||
"./package/components/icons/store.d.ts"
|
||||
],
|
||||
"tailwindUtils": [
|
||||
"./package/components/apps/editor/componentsPanel/tailwindUtils.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { type Preview, ResourceService, UserService } from '$lib/gen'
|
||||
import type { Text } from 'yjs'
|
||||
import { initializeVscode } from '$lib/components/vscode'
|
||||
import { initializeVscode, keepModelAroundToAvoidDisposalOfWorkers } from '$lib/components/vscode'
|
||||
|
||||
import { initializeMode } from 'monaco-graphql/esm/initializeMode.js'
|
||||
import type { MonacoGraphQLAPI } from 'monaco-graphql/esm/api.js'
|
||||
@@ -133,6 +133,7 @@
|
||||
import EditorTheme from './EditorTheme.svelte'
|
||||
import {
|
||||
BIGQUERY_TYPES,
|
||||
DUCKDB_TYPES,
|
||||
MSSQL_TYPES,
|
||||
MYSQL_TYPES,
|
||||
ORACLEDB_TYPES,
|
||||
@@ -482,7 +483,9 @@
|
||||
? MSSQL_TYPES
|
||||
: scriptLang === 'oracledb'
|
||||
? ORACLEDB_TYPES
|
||||
: []
|
||||
: scriptLang === 'duckdb'
|
||||
? DUCKDB_TYPES
|
||||
: []
|
||||
).map((t) => ({
|
||||
label: t,
|
||||
kind: languages.CompletionItemKind.Function,
|
||||
@@ -1263,6 +1266,8 @@
|
||||
folding
|
||||
})
|
||||
|
||||
keepModelAroundToAvoidDisposalOfWorkers()
|
||||
|
||||
// updateEditorKeybindingsMode(editor, 'vim', undefined)
|
||||
|
||||
let ataModel: NodeJS.Timeout | undefined = undefined
|
||||
@@ -1352,6 +1357,7 @@
|
||||
try {
|
||||
closeWebsockets()
|
||||
vimDisposable?.dispose()
|
||||
console.log('disposing editor', editor, model)
|
||||
model?.dispose()
|
||||
editor && editor.dispose()
|
||||
console.log('disposed editor')
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
return sql
|
||||
case 'oracledb':
|
||||
return sql
|
||||
case 'duckdb':
|
||||
return sql
|
||||
case 'powershell':
|
||||
return powershell
|
||||
case 'php':
|
||||
@@ -66,10 +68,10 @@
|
||||
case 'nu':
|
||||
return python
|
||||
case 'ansible':
|
||||
return yaml;
|
||||
return yaml
|
||||
case 'java':
|
||||
return java;
|
||||
// for related places search: ADD_NEW_LANG
|
||||
return java
|
||||
// for related places search: ADD_NEW_LANG
|
||||
default:
|
||||
return typescript
|
||||
}
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
import '@codingame/monaco-vscode-standalone-css-language-features'
|
||||
import '@codingame/monaco-vscode-standalone-typescript-language-features'
|
||||
import '@codingame/monaco-vscode-standalone-html-language-features'
|
||||
import {
|
||||
editor as meditor,
|
||||
KeyCode,
|
||||
KeyMod,
|
||||
Uri as mUri,
|
||||
languages,
|
||||
type IRange,
|
||||
type IDisposable
|
||||
} from 'monaco-editor'
|
||||
|
||||
languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
target: languages.typescript.ScriptTarget.Latest,
|
||||
allowNonTsExtensions: true,
|
||||
@@ -44,15 +54,15 @@
|
||||
import { BROWSER } from 'esm-env'
|
||||
|
||||
import { createHash, editorConfig, langToExt, updateOptions } from '$lib/editorUtils'
|
||||
import {
|
||||
editor as meditor,
|
||||
KeyCode,
|
||||
KeyMod,
|
||||
Uri as mUri,
|
||||
languages,
|
||||
type IRange,
|
||||
type IDisposable
|
||||
} from 'monaco-editor'
|
||||
// import {
|
||||
// editor as meditor,
|
||||
// KeyCode,
|
||||
// KeyMod,
|
||||
// Uri as mUri,
|
||||
// languages,
|
||||
// type IRange,
|
||||
// type IDisposable
|
||||
// } from 'monaco-editor'
|
||||
|
||||
import { allClasses } from './apps/editor/componentsPanel/cssUtils'
|
||||
|
||||
@@ -60,7 +70,7 @@
|
||||
|
||||
import libStdContent from '$lib/es6.d.ts.txt?raw'
|
||||
import domContent from '$lib/dom.d.ts.txt?raw'
|
||||
import { initializeVscode } from './vscode'
|
||||
import { initializeVscode, keepModelAroundToAvoidDisposalOfWorkers } from './vscode'
|
||||
import EditorTheme from './EditorTheme.svelte'
|
||||
import { vimMode } from '$lib/stores'
|
||||
import { initVim } from './monaco_keybindings'
|
||||
@@ -348,6 +358,7 @@
|
||||
snippetsPreventQuickSuggestions: disableSuggestions
|
||||
}
|
||||
})
|
||||
keepModelAroundToAvoidDisposalOfWorkers()
|
||||
|
||||
let timeoutModel: NodeJS.Timeout | undefined = undefined
|
||||
editor.onDidChangeModelContent((event) => {
|
||||
@@ -467,33 +478,46 @@
|
||||
}
|
||||
|
||||
function addTailwindClassCompletions() {
|
||||
// Define a custom word definition for Tailwind classes
|
||||
languages.setMonarchTokensProvider('tailwindcss', {
|
||||
tokenizer: {
|
||||
root: [[/[a-zA-Z0-9-]+/, 'tailwind-class']]
|
||||
}
|
||||
})
|
||||
|
||||
languages.registerCompletionItemProvider('tailwindcss', {
|
||||
triggerCharacters: ['-'],
|
||||
provideCompletionItems: function (model, position, context, token) {
|
||||
const word = model.getWordUntilPosition(position)
|
||||
const wordUntilPosition = model.getWordUntilPosition(position)
|
||||
const lineContent = model.getLineContent(position.lineNumber)
|
||||
|
||||
// Get the text from the start of the line to the cursor
|
||||
const textUntilPosition = lineContent.substring(0, position.column - 1)
|
||||
// Find the last space before the cursor
|
||||
const lastSpaceIndex = textUntilPosition.lastIndexOf(' ')
|
||||
const startColumn = lastSpaceIndex === -1 ? 1 : lastSpaceIndex + 2
|
||||
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: word.startColumn,
|
||||
startColumn: startColumn,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: word.endColumn
|
||||
endColumn: position.column
|
||||
}
|
||||
|
||||
if (word && word.word) {
|
||||
const currentWord = word.word
|
||||
const currentWord = wordUntilPosition.word
|
||||
|
||||
const suggestions = tailwindClasses
|
||||
.filter((className) => className.includes(currentWord))
|
||||
.map((className) => ({
|
||||
label: className,
|
||||
kind: languages.CompletionItemKind.Class,
|
||||
insertText: className,
|
||||
documentation: 'Custom CSS class',
|
||||
range: range
|
||||
}))
|
||||
const suggestions = tailwindClasses
|
||||
.filter((className) => className.includes(currentWord))
|
||||
.map((className) => ({
|
||||
label: className,
|
||||
kind: languages.CompletionItemKind.Class,
|
||||
insertText: className,
|
||||
documentation: 'Tailwind CSS class',
|
||||
range: range,
|
||||
preselect: true
|
||||
}))
|
||||
|
||||
return { suggestions }
|
||||
}
|
||||
|
||||
return { suggestions: [] }
|
||||
return { suggestions }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
<script module>
|
||||
import '@codingame/monaco-vscode-standalone-typescript-language-features'
|
||||
|
||||
languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
target: languages.typescript.ScriptTarget.Latest,
|
||||
allowNonTsExtensions: true,
|
||||
noSemanticValidation: false,
|
||||
noLib: true,
|
||||
moduleResolution: languages.typescript.ModuleResolutionKind.NodeJs
|
||||
})
|
||||
languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
||||
noSemanticValidation: false,
|
||||
noSyntaxValidation: false,
|
||||
noSuggestionDiagnostics: false,
|
||||
diagnosticCodesToIgnore: [1108]
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { BROWSER } from 'esm-env'
|
||||
import {
|
||||
@@ -422,20 +440,6 @@
|
||||
await initializeVscode('templateEditor')
|
||||
console.log('initialized')
|
||||
initialized = true
|
||||
languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
target: languages.typescript.ScriptTarget.Latest,
|
||||
allowNonTsExtensions: true,
|
||||
noSemanticValidation: false,
|
||||
noLib: true,
|
||||
moduleResolution: languages.typescript.ModuleResolutionKind.NodeJs
|
||||
})
|
||||
|
||||
languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
||||
noSemanticValidation: false,
|
||||
noSyntaxValidation: false,
|
||||
noSuggestionDiagnostics: false,
|
||||
diagnosticCodesToIgnore: [1108]
|
||||
})
|
||||
|
||||
languages.register({ id: 'template' })
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
usePointerDownOutside={true}
|
||||
clickOutsideExcludeIds={modalId ? [modalId] : []}
|
||||
let:close
|
||||
renderContent
|
||||
>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<MeltButton meltElement={trigger} class="w-full h-full">
|
||||
|
||||
@@ -786,7 +786,6 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="stylesheet" href="/tailwind_full.css" />
|
||||
</svelte:head>
|
||||
|
||||
<DarkModeObserver on:change={onThemeChange} />
|
||||
|
||||
@@ -243,7 +243,6 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="stylesheet" href="/tailwind_full.css" />
|
||||
</svelte:head>
|
||||
|
||||
<DarkModeObserver on:change={onThemeChange} />
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import CSharpIcon from '$lib/components/icons/CSharpIcon.svelte'
|
||||
import NuIcon from '$lib/components/icons/NuIcon.svelte'
|
||||
import JavaIcon from '$lib/components/icons/JavaIcon.svelte'
|
||||
import DuckDbIcon from '$lib/components/icons/DuckDbIcon.svelte'
|
||||
|
||||
export let lang:
|
||||
| SupportedLanguage
|
||||
@@ -49,6 +50,7 @@
|
||||
postgresql: 'Postgresql',
|
||||
bigquery: 'BigQuery',
|
||||
oracledb: 'Oracle Database',
|
||||
duckdb: 'DuckDB',
|
||||
snowflake: 'Snowflake',
|
||||
mysql: 'MySQL',
|
||||
mssql: 'MS SQL Server',
|
||||
@@ -59,7 +61,7 @@
|
||||
csharp: 'C#',
|
||||
nu: 'Nu',
|
||||
java: 'Java'
|
||||
// for related places search: ADD_NEW_LANG
|
||||
// for related places search: ADD_NEW_LANG
|
||||
}
|
||||
|
||||
const langToComponent: Record<
|
||||
@@ -91,8 +93,9 @@
|
||||
ansible: AnsibleIcon,
|
||||
csharp: CSharpIcon,
|
||||
nu: NuIcon,
|
||||
java: JavaIcon
|
||||
// for related places search: ADD_NEW_LANG
|
||||
java: JavaIcon,
|
||||
duckdb: DuckDbIcon
|
||||
// for related places search: ADD_NEW_LANG
|
||||
}
|
||||
|
||||
let subIconScale = width === 30 ? 0.6 : 0.8
|
||||
|
||||
@@ -161,15 +161,15 @@ export class Autocompletor {
|
||||
modifiedCode: string = ''
|
||||
applyZone:
|
||||
| {
|
||||
startLineNumber: number
|
||||
endLineNumber: number
|
||||
}
|
||||
startLineNumber: number
|
||||
endLineNumber: number
|
||||
}
|
||||
| undefined = undefined
|
||||
lastChangePosition:
|
||||
| {
|
||||
lineNumber: number
|
||||
column: number
|
||||
}
|
||||
lineNumber: number
|
||||
column: number
|
||||
}
|
||||
| undefined = undefined
|
||||
|
||||
abortController: AbortController | undefined = undefined
|
||||
@@ -180,12 +180,12 @@ export class Autocompletor {
|
||||
|
||||
predictedChange:
|
||||
| {
|
||||
position: {
|
||||
lineNumber: number
|
||||
column: number
|
||||
}
|
||||
distance: number
|
||||
}
|
||||
position: {
|
||||
lineNumber: number
|
||||
column: number
|
||||
}
|
||||
distance: number
|
||||
}
|
||||
| undefined = undefined
|
||||
tabWidget: meditor.IContentWidget | undefined = undefined
|
||||
|
||||
@@ -240,18 +240,18 @@ export class Autocompletor {
|
||||
|
||||
let closestPosition:
|
||||
| {
|
||||
lineNumber: number
|
||||
column: number
|
||||
}
|
||||
lineNumber: number
|
||||
column: number
|
||||
}
|
||||
| undefined = undefined
|
||||
let closestDistance = Infinity
|
||||
for (const change of this.visualChanges) {
|
||||
if (change.type === 'deleted') {
|
||||
const distance = Math.min(
|
||||
Math.abs(change.range.startLine - position.lineNumber) +
|
||||
Math.abs(change.range.startColumn - position.column) / 10000,
|
||||
Math.abs(change.range.startColumn - position.column) / 10000,
|
||||
Math.abs(change.range.endLine - position.lineNumber) +
|
||||
Math.abs(change.range.endColumn - position.column) / 10000
|
||||
Math.abs(change.range.endColumn - position.column) / 10000
|
||||
)
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance
|
||||
|
||||
@@ -166,7 +166,7 @@ export function graphBuilder(
|
||||
disableMoveIds: options?.disableMoveIds,
|
||||
enableTrigger: sourceId === 'Input',
|
||||
// If the index is -1, it means that the target module is not in the modules array, so we set it to the length of the array
|
||||
index: index >= 0 ? index : mods?.length ?? 0,
|
||||
index: index >= 0 ? index : (mods?.length ?? 0),
|
||||
...extra,
|
||||
insertable: extra.insertable && !options?.disableInsert && prefix == undefined
|
||||
}
|
||||
@@ -635,7 +635,7 @@ export function graphBuilder(
|
||||
if (failureModule) {
|
||||
let toAdd: Record<string, string> = {}
|
||||
Object.keys(extra.flowModuleStates ?? {}).forEach((id) => {
|
||||
if (id.startsWith('failure-')) {
|
||||
if (id.startsWith('failure')) {
|
||||
const failureState = extra.flowModuleStates?.[id] as GraphModuleState | undefined
|
||||
if (failureState?.parent_module) {
|
||||
toAdd[failureState.parent_module] = id
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<script>
|
||||
export let width = 16
|
||||
export let height = 16
|
||||
</script>
|
||||
|
||||
<svg {width} {height} viewBox="0 0 335 335" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M167.2 334.4C74.7 334.4 0 259.7 0 167.2C0 74.7 74.7 0 167.2 0C259.7 0 334.4 74.7 334.4 167.2C334.4 259.7 259.7 334.4 167.2 334.4Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M256.6 142.4H223.8V191.9H256.6C270.2 191.9 281.5 180.6 281.5 167C281.5 153.3 270.2 142.4 256.6 142.4Z"
|
||||
fill="#FFF100"
|
||||
/>
|
||||
<path
|
||||
d="M63.1 167.2C63.1 205.4 94.2 236.5 132.4 236.5C170.6 236.5 201.7 205.4 201.7 167.2C201.7 129 170.6 97.9 132.4 97.9C94.2 97.9 63.1 129 63.1 167.2Z"
|
||||
fill="#FFF100"
|
||||
/>
|
||||
</svg>
|
||||
@@ -18,6 +18,7 @@
|
||||
export let clickOutsideExcludeIds: string[] = []
|
||||
export let menuClass: string = ''
|
||||
export let open = false
|
||||
export let renderContent: boolean = false
|
||||
|
||||
// Use the passed createMenu function
|
||||
const {
|
||||
@@ -81,7 +82,7 @@
|
||||
</button>
|
||||
|
||||
<!--svelte-ignore a11y-no-static-element-interactions-->
|
||||
{#if open}
|
||||
{#if open || renderContent}
|
||||
<div
|
||||
use:melt={$menuElement}
|
||||
class={twMerge(
|
||||
|
||||
@@ -4,7 +4,7 @@ import { initServices } from 'monaco-languageclient/vscode/services'
|
||||
import getMonarchServiceOverride from '@codingame/monaco-vscode-monarch-service-override'
|
||||
import '@codingame/monaco-vscode-standalone-typescript-language-features'
|
||||
import getConfigurationServiceOverride from '@codingame/monaco-vscode-configuration-service-override'
|
||||
import { editor as meditor } from 'monaco-editor/esm/vs/editor/editor.api'
|
||||
import { editor as meditor, Uri as mUri } from 'monaco-editor'
|
||||
|
||||
export let isInitialized = false
|
||||
export let isInitializing = false
|
||||
@@ -267,3 +267,10 @@ export async function initializeVscode(caller?: string, htmlContainer?: HTMLElem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function keepModelAroundToAvoidDisposalOfWorkers() {
|
||||
const keepEditorUri = mUri.parse('file:///avoidDisposalOfWorkers')
|
||||
if (!meditor?.getModel(keepEditorUri)) {
|
||||
meditor.createModel('', 'typescript', keepEditorUri)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,56 @@ export const MSSQL_TYPES = [
|
||||
'decimal',
|
||||
'bit'
|
||||
]
|
||||
export const DUCKDB_TYPES = [
|
||||
'BIGINT',
|
||||
'INT8',
|
||||
'LONG',
|
||||
'BIT',
|
||||
'BITSTRING',
|
||||
'BLOB',
|
||||
'BYTEA',
|
||||
'BINARY',
|
||||
'VARBINARY',
|
||||
'BOOLEAN',
|
||||
'BOOL',
|
||||
'LOGICAL',
|
||||
'DATE',
|
||||
'DECIMAL',
|
||||
'NUMERIC',
|
||||
'DOUBLE',
|
||||
'FLOAT8',
|
||||
'FLOAT',
|
||||
'FLOAT4',
|
||||
'REAL',
|
||||
'HUGEINT',
|
||||
'INTEGER',
|
||||
'INT4',
|
||||
'INT',
|
||||
'SIGNED',
|
||||
'INTERVAL',
|
||||
'JSON',
|
||||
'SMALLINT',
|
||||
'INT2',
|
||||
'SHORT',
|
||||
'TIME',
|
||||
'TIMESTAMP WITH TIME ZONE',
|
||||
'TIMESTAMPTZ',
|
||||
'TIMESTAMP',
|
||||
'DATETIME',
|
||||
'TINYINT',
|
||||
'INT1',
|
||||
'UBIGINT',
|
||||
'UHUGEINT',
|
||||
'UINTEGER',
|
||||
'USMALLINT',
|
||||
'UTINYINT',
|
||||
'UUID',
|
||||
'VARCHAR',
|
||||
'CHAR',
|
||||
'BPCHAR',
|
||||
'TEXT',
|
||||
'STRING'
|
||||
]
|
||||
|
||||
export const DB_TYPES: Record<DbType, string[]> = {
|
||||
bigquery: BIGQUERY_TYPES,
|
||||
|
||||
@@ -9,6 +9,7 @@ import initRegexParsers, {
|
||||
parse_sql,
|
||||
parse_mysql,
|
||||
parse_oracledb,
|
||||
parse_duckdb,
|
||||
parse_bigquery,
|
||||
parse_snowflake,
|
||||
parse_graphql,
|
||||
@@ -96,7 +97,9 @@ export async function inferArgs(
|
||||
}
|
||||
|
||||
let inlineDBResource: string | undefined = undefined
|
||||
if (['postgresql', 'mysql', 'bigquery', 'snowflake', 'mssql', 'oracledb'].includes(language ?? '')) {
|
||||
if (
|
||||
['postgresql', 'mysql', 'bigquery', 'snowflake', 'mssql', 'oracledb'].includes(language ?? '')
|
||||
) {
|
||||
await initWasmRegex()
|
||||
inlineDBResource = parse_db_resource(code)
|
||||
}
|
||||
@@ -147,6 +150,9 @@ export async function inferArgs(
|
||||
...inferedSchema.args
|
||||
]
|
||||
}
|
||||
} else if (language == 'duckdb') {
|
||||
await initWasmRegex()
|
||||
inferedSchema = JSON.parse(parse_duckdb(code))
|
||||
} else if (language == 'snowflake') {
|
||||
inferedSchema = JSON.parse(parse_snowflake(code))
|
||||
if (inlineDBResource === undefined) {
|
||||
@@ -194,7 +200,7 @@ export async function inferArgs(
|
||||
} else if (language == 'java') {
|
||||
await initWasmJava()
|
||||
inferedSchema = JSON.parse(parse_java(code))
|
||||
// for related places search: ADD_NEW_LANG
|
||||
// for related places search: ADD_NEW_LANG
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -5,23 +5,21 @@ export function argSigToJsonSchemaType(
|
||||
| string
|
||||
| { resource: string | null }
|
||||
| {
|
||||
list:
|
||||
| (string | { object: { key: string; typ: any }[] })
|
||||
| { str: any }
|
||||
| { object: { key: string; typ: any }[] }
|
||||
| null
|
||||
}
|
||||
list:
|
||||
| (string | { object: { key: string; typ: any }[] })
|
||||
| { str: any }
|
||||
| { object: { key: string; typ: any }[] }
|
||||
| null
|
||||
}
|
||||
| { dynselect: string }
|
||||
| { str: string[] | null }
|
||||
| { object: { key: string; typ: any }[] }
|
||||
| {
|
||||
oneof: [
|
||||
{
|
||||
oneof: {
|
||||
label: string
|
||||
properties: { key: string; typ: any }[]
|
||||
}
|
||||
]
|
||||
},
|
||||
}[]
|
||||
},
|
||||
oldS: SchemaProperty
|
||||
): void {
|
||||
const newS: SchemaProperty = { type: '' }
|
||||
|
||||
@@ -307,6 +307,19 @@ INSERT INTO demo VALUES (@P1, @P2);
|
||||
UPDATE demo SET col2 = @P3 WHERE col2 = @P2;
|
||||
`
|
||||
|
||||
const DUCKDB_INIT_CODE = `-- $friends_csv (s3object)
|
||||
-- $name (text) = Ben
|
||||
|
||||
ATTACH '$res:u/demo/amazed_postgresql' AS db (TYPE postgres);
|
||||
CREATE TABLE IF NOT EXISTS db.public.friends (name text);
|
||||
|
||||
INSERT INTO db.public.friends
|
||||
SELECT name FROM read_csv($friends_csv);
|
||||
|
||||
SELECT 'Hello ' || $name || ', you have ' ||
|
||||
(SELECT COUNT(*) FROM read_csv($friends_csv)) || ' new friends !';
|
||||
`
|
||||
|
||||
const GRAPHQL_INIT_CODE = `query($name4: String, $name2: Int, $name3: [String]) {
|
||||
demo(name1: $name1, name2: $name2, name3: $name3) {
|
||||
name1,
|
||||
@@ -1134,6 +1147,9 @@ export const INITIAL_CODE = {
|
||||
mssql: {
|
||||
script: MSSQL_INIT_CODE
|
||||
},
|
||||
duckdb: {
|
||||
script: DUCKDB_INIT_CODE
|
||||
},
|
||||
graphql: {
|
||||
script: GRAPHQL_INIT_CODE
|
||||
},
|
||||
@@ -1258,6 +1274,8 @@ export function initialCode(
|
||||
return INITIAL_CODE.mssql.script
|
||||
} else if (language == 'graphql') {
|
||||
return INITIAL_CODE.graphql.script
|
||||
} else if (language == 'duckdb') {
|
||||
return INITIAL_CODE.duckdb.script
|
||||
} else if (language == 'php') {
|
||||
return INITIAL_CODE.php.script
|
||||
} else if (language == 'rust') {
|
||||
|
||||
@@ -38,6 +38,8 @@ export function scriptLangToEditorLang(
|
||||
return 'sql'
|
||||
} else if (lang == 'mssql') {
|
||||
return 'sql'
|
||||
} else if (lang == 'duckdb') {
|
||||
return 'sql'
|
||||
} else if (lang == 'python3') {
|
||||
return 'python'
|
||||
} else if (lang == 'bash') {
|
||||
@@ -140,7 +142,8 @@ const scriptLanguagesArray: [SupportedLanguage | 'docker' | 'bunnative', string]
|
||||
['csharp', 'C#'],
|
||||
['docker', 'Docker'],
|
||||
['nu', 'Nu'],
|
||||
['java', 'Java']
|
||||
['java', 'Java'],
|
||||
['duckdb', 'DuckDB']
|
||||
// for related places search: ADD_NEW_LANG
|
||||
]
|
||||
export function processLangs(selected: string | undefined, langs: string[]): string[] {
|
||||
@@ -150,7 +153,7 @@ export function processLangs(selected: string | undefined, langs: string[]): str
|
||||
let ls = langs.filter((lang) => lang !== 'nativets')
|
||||
|
||||
//those languages are newer and may not be in the saved list
|
||||
let nl = ['bunnative', 'rust', 'ansible', 'csharp', 'nu', 'java']
|
||||
let nl = ['bunnative', 'rust', 'ansible', 'csharp', 'nu', 'java', 'duckdb']
|
||||
// for related places search: ADD_NEW_LANG
|
||||
nl.forEach((lang) => {
|
||||
if (!ls.includes(lang)) {
|
||||
|
||||
@@ -755,7 +755,7 @@
|
||||
<Badge color="blue">priority: {job.priority}</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
{#if job.tag && !['deno', 'python3', 'flow', 'other', 'go', 'postgresql', 'mysql', 'bigquery', 'snowflake', 'mssql', 'graphql', 'oracledb', 'nativets', 'bash', 'powershell', 'php', 'rust', 'other', 'ansible', 'csharp', 'nu', 'java', 'dependency'].includes(job.tag)}
|
||||
{#if job.tag && !['deno', 'python3', 'flow', 'other', 'go', 'postgresql', 'mysql', 'bigquery', 'snowflake', 'mssql', 'graphql', 'oracledb', 'nativets', 'bash', 'powershell', 'php', 'rust', 'other', 'ansible', 'csharp', 'nu', 'java', 'duckdb', 'dependency'].includes(job.tag)}
|
||||
<!-- for related places search: ADD_NEW_LANG -->
|
||||
<div>
|
||||
<Badge color="indigo">Tag: {job.tag}</Badge>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const plugin = require('tailwindcss/plugin')
|
||||
const { tailwindClasses } = require('./src/lib/components/apps/editor/componentsPanel/tailwindUtils')
|
||||
|
||||
const lightTheme = {
|
||||
surface: '#ffffff',
|
||||
@@ -80,7 +81,8 @@ const config = {
|
||||
'autocomplete-list-item',
|
||||
'autocomplete-list-item-create',
|
||||
'selected',
|
||||
'wm-tab-selected'
|
||||
'wm-tab-selected',
|
||||
...tailwindClasses
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.492.1"
|
||||
wmill_pg = ">=1.492.1"
|
||||
wmill = ">=1.493.0"
|
||||
wmill_pg = ">=1.493.0"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.492.1
|
||||
version: 1.493.0
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.492.1'
|
||||
ModuleVersion = '1.493.0'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.492.1"
|
||||
version = "1.493.0"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill-pg"
|
||||
version = "1.492.1"
|
||||
version = "1.493.0"
|
||||
description = "An extension client for the wmill client library focused on pg"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.492.1",
|
||||
"version": "1.493.0",
|
||||
"exports": "./src/index.ts",
|
||||
"publish": {
|
||||
"exclude": ["!src", "./s3Types.ts", "./client.ts"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-client",
|
||||
"description": "Windmill SDK client for browsers and Node.js",
|
||||
"version": "1.492.1",
|
||||
"version": "1.493.0",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"devDependencies": {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.492.1
|
||||
1.493.0
|
||||
|
||||
Reference in New Issue
Block a user